diff --git a/.circleci/config.yml b/.circleci/config.yml index dac6a9c5f..7775bac69 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,7 +15,7 @@ orbs: # defaults # # YAML defaults templates, in alphabetical order -## +## defaults_Dependencies: &defaults_Dependencies | apk --no-cache add git apk --no-cache add ca-certificates @@ -50,7 +50,7 @@ defaults_license_scanner: &defaults_license_scanner executors: default-docker: working_directory: /home/circleci/project - docker: + docker: - image: node:12.16.1-alpine default-machine: @@ -66,10 +66,10 @@ jobs: setup: executor: default-docker steps: - - checkout - run: name: Install general dependencies command: *defaults_Dependencies + - checkout - run: name: Access npm folder as root command: cd $(npm root -g)/npm @@ -87,10 +87,10 @@ jobs: test-unit: executor: default-docker steps: - - checkout - run: name: Install general dependencies command: *defaults_Dependencies + - checkout - restore_cache: keys: - dependency-cache-5-{{ checksum "src/package.json" }} @@ -129,10 +129,10 @@ jobs: lint: executor: default-docker steps: - - checkout - run: name: Install general dependencies command: *defaults_Dependencies + - checkout - restore_cache: keys: - dependency-cache-5-{{ checksum "src/package.json" }} @@ -179,7 +179,7 @@ jobs: root: /tmp paths: - ./docker-image.tar - + license-scan: executor: default-machine steps: @@ -249,7 +249,7 @@ jobs: # failure_message: 'Anchore Image Scan failed for: \`"${DOCKER_ORG}/${CIRCLE_PROJECT_REPONAME}:${CIRCLE_TAG}"\`' # - store_artifacts: # path: anchore-reports - + publish: executor: default-machine steps: @@ -271,8 +271,17 @@ jobs: command: | echo "Publishing $DOCKER_ORG/$CIRCLE_PROJECT_REPONAME:$CIRCLE_TAG" docker push $DOCKER_ORG/$CIRCLE_PROJECT_REPONAME:$CIRCLE_TAG - echo "Publishing $DOCKER_ORG/$CIRCLE_PROJECT_REPONAME:$RELEASE_TAG" - docker push $DOCKER_ORG/$CIRCLE_PROJECT_REPONAME:$RELEASE_TAG + case "$CIRCLE_TAG" in + *-pisp*) + # Don't update `late5t` for an image that has a `-pisp` + echo 'skipping late5t tag' + exit 0 + ;; + *) + echo "Publishing $DOCKER_ORG/$CIRCLE_PROJECT_REPONAME:$RELEASE_TAG" + docker push $DOCKER_ORG/$CIRCLE_PROJECT_REPONAME:$RELEASE_TAG + ;; + esac - slack/status: webhook: "$SLACK_WEBHOOK_ANNOUNCEMENT" success_message: '*"${CIRCLE_PROJECT_REPONAME}"* - Release \`"${CIRCLE_TAG}"\` \nhttps://github.com/mojaloop/"${CIRCLE_PROJECT_REPONAME}"/releases/tag/"${CIRCLE_TAG}"' @@ -344,7 +353,7 @@ workflows: - audit-licenses filters: tags: - only: /v[0-9]+(\.[0-9]+)*(\-snapshot)?(\-hotfix(\.[0-9]+))?/ + only: /v[0-9]+(\.[0-9]+)*(\-snapshot)?(\-hotfix(\.[0-9]+))?(\-pisp)?/ branches: ignore: - /.*/ @@ -354,7 +363,7 @@ workflows: - build filters: tags: - only: /v[0-9]+(\.[0-9]+)*(\-snapshot)?(\-hotfix(\.[0-9]+))?/ + only: /v[0-9]+(\.[0-9]+)*(\-snapshot)?(\-hotfix(\.[0-9]+))?(\-pisp)?/ branches: ignore: - /.*/ @@ -375,7 +384,7 @@ workflows: - license-scan filters: tags: - only: /v[0-9]+(\.[0-9]+)*(\-snapshot)?(\-hotfix(\.[0-9]+))?/ + only: /v[0-9]+(\.[0-9]+)*(\-snapshot)?(\-hotfix(\.[0-9]+))?(\-pisp)?/ branches: ignore: - /.*/ diff --git a/.gitignore b/.gitignore index 35783b086..69c3e7865 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ .swp src/junit.xml +.DS_Store +.vscode +secrets/*.pem diff --git a/Dockerfile b/Dockerfile index 91427653a..081cff1e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12.16.1-alpine as builder +FROM node:12.18.3-alpine as builder RUN apk add --no-cache git python build-base @@ -10,17 +10,18 @@ WORKDIR /src/ # files change- only when any dependencies change- which is a superior developer experience when # relying on docker-compose. COPY ./src/package.json ./package.json +COPY ./src/package-lock.json ./package-lock.json COPY ./src/lib/cache/package.json ./lib/cache/package.json -COPY ./src/lib/log/package.json ./lib/log/package.json +COPY ./src/lib/check/package.json ./lib/check/package.json COPY ./src/lib/model/lib/requests/package.json ./lib/model/lib/requests/package.json COPY ./src/lib/model/lib/shared/package.json ./lib/model/lib/shared/package.json COPY ./src/lib/model/package.json ./lib/model/package.json COPY ./src/lib/randomphrase/package.json ./lib/randomphrase/package.json COPY ./src/lib/router/package.json ./lib/router/package.json COPY ./src/lib/validate/package.json ./lib/validate/package.json -RUN npm install +RUN npm ci --only=production -FROM node:12.16.1-alpine +FROM node:12.18.3-alpine ARG BUILD_DATE ARG VCS_URL @@ -37,7 +38,6 @@ LABEL org.label-schema.url="https://mojaloop.io/" LABEL org.label-schema.version=$VERSION COPY --from=builder /src/ /src -RUN npm prune --production COPY ./src ./src COPY ./secrets / diff --git a/Dockerfile-integration b/Dockerfile-integration new file mode 100644 index 000000000..cae1662a0 --- /dev/null +++ b/Dockerfile-integration @@ -0,0 +1,44 @@ +FROM node:12.18.3-alpine as builder + +RUN apk add --no-cache git python build-base + +EXPOSE 3000 + +WORKDIR /src/ + +# This is super-ugly, but it means we don't have to re-run npm install every time any of the source +# files change- only when any dependencies change- which is a superior developer experience when +# relying on docker-compose. +COPY ./src/package.json ./package.json +COPY ./src/package-lock.json ./package-lock.json +COPY ./src/lib/cache/package.json ./lib/cache/package.json +COPY ./src/lib/check/package.json ./lib/check/package.json +COPY ./src/lib/model/lib/requests/package.json ./lib/model/lib/requests/package.json +COPY ./src/lib/model/lib/shared/package.json ./lib/model/lib/shared/package.json +COPY ./src/lib/model/package.json ./lib/model/package.json +COPY ./src/lib/randomphrase/package.json ./lib/randomphrase/package.json +COPY ./src/lib/router/package.json ./lib/router/package.json +COPY ./src/lib/validate/package.json ./lib/validate/package.json +RUN npm ci + +FROM node:12.18.3-alpine + +ARG BUILD_DATE +ARG VCS_URL +ARG VCS_REF +ARG VERSION + +# See http://label-schema.org/rc1/ for label schema info +LABEL org.label-schema.schema-version="1.0" +LABEL org.label-schema.name="sdk-scheme-adapter" +LABEL org.label-schema.build-date=$BUILD_DATE +LABEL org.label-schema.vcs-url=$VCS_URL +LABEL org.label-schema.vcs-ref=$VCS_REF +LABEL org.label-schema.url="https://mojaloop.io/" +LABEL org.label-schema.version=$VERSION + +COPY --from=builder /src/ /src +COPY ./src ./src +COPY ./secrets / + +CMD ["node", "src/index.js"] diff --git a/README.md b/README.md index 26a7b0f14..a58b8ee56 100644 --- a/README.md +++ b/README.md @@ -153,4 +153,19 @@ docker exec -it scheme-adapter-int sh -c 'npm run test:int' # copy results out docker cp scheme-adapter-int:/src/junit.xml . -``` \ No newline at end of file +``` + +### Get status of quote request +The status of a previously sent quotation request can be get by executing `GET /quotes/{ID}`. +When the response to the original quote request is sent, the response is cached in the redis store. When a `GET /quotes/{ID}` is received, +the cached response is retrieved from the redis store and returned to the caller as a body with `PUT /quotes/{ID}` request. +When the redis is setup as a persistent store then it will return the response for all the quote requests sent with `POST /quotes`. If the the redis is setup as cache with expiry time then it will not return response for the expired quotes when the cache expires. Also only the payer dfsp is supposed to make the `GET /quotes/{ID}` request. +If the quote response is not found in the redis store `PUT /quotes/{ID}` will be made with the following body +``` +{ + "errorInformation":{ + "errorCode":"3205", + "errorDescription":"Quote ID not found" + } +} +``` diff --git a/docker-compose.integration.yml b/docker-compose.integration.yml index a2d9ad359..68c3ef327 100644 --- a/docker-compose.integration.yml +++ b/docker-compose.integration.yml @@ -11,6 +11,7 @@ services: container_name: scheme-adapter-int build: context: . + dockerfile: Dockerfile-integration target: builder env_file: ./src/test/config/integration.env volumes: diff --git a/docs/dfspInboundApi.yaml b/docs/dfspInboundApi.yaml index 53d0d0ae5..ad3e95fc4 100644 --- a/docs/dfspInboundApi.yaml +++ b/docs/dfspInboundApi.yaml @@ -9,7 +9,7 @@ info: license: name: Apache License Version 2.0, January 2004 url: http://www.apache.org/licenses/ - version: 1.0.0 + version: 1.1.0 paths: /participants/{idType}/{idValue}: @@ -171,6 +171,24 @@ paths: $ref: '#/components/schemas/transferDetailsResponse' 500: $ref: '#/components/responses/500' + put: + summary: Receive notification for a specific transfer + description: The HTTP request `PUT /transfers/{transferId}` is used to receive notification for transfer being fulfiled when the FSP is a Payee + tags: + - Transfers + parameters: + - $ref: '#/components/schemas/transferId' + requestBody: + description: An incoming notification for fulfiled transfer + content: + application/json: + schema: + $ref: '#/components/schemas/fulfilNotification' + responses: + 200: + description: The notification was accepted + 500: + $ref: '#/components/responses/500' components: schemas: @@ -345,6 +363,8 @@ components: - XDR - XOF - XPF + - XTS + - XXX - YER - ZAR - ZMW @@ -380,6 +400,13 @@ components: minItems: 0 maxItems: 16 + accountList: + type: array + items: + $ref: '#/components/schemas/account' + minItems: 1 + maxItems: 32 + geoCode: type: object description: Indicates the geographic location from where the transaction was initiated. @@ -478,6 +505,23 @@ components: minLength: 1 maxLength: 128 + + account: + type: object + properties: + address: + type: string + minLength: 1 + maxLength: 1023 + currency: + type: string + minLength: 3 + maxLength: 3 + description: + type: string + minLength: 1 + maxLength: 128 + initiatorType: type: string enum: @@ -680,6 +724,8 @@ components: description: Up to 4 digits specifying the sender's merchant classification, if known and applicable. extensionList: $ref: '#/components/schemas/extensionList' + accounts: + $ref: '#/components/schemas/accountList' transferState: type: string @@ -774,7 +820,6 @@ components: extensions: $ref: '#/components/schemas/extensionList' - transferStatus: type: string enum: @@ -782,6 +827,26 @@ components: - WAITING_FOR_QUOTE_ACCEPTANCE - COMPLETED + fulfilNotification: + title: TransfersIDPatchResponse + type: object + description: PUT /transfers/{transferId} object + properties: + completedTimestamp: + $ref: '#/components/schemas/timestamp' + description: Time and date when the transaction was completed. + example: "2020-05-19T08:38:08.699-04:00" + transferState: + $ref: '#/components/schemas/transferState' + description: State of the transfer. + example: COMMITTED + extensionList: + $ref: '#/components/schemas/ExtensionList' + description: Optional extension, specific to deployment. + required: + - completedTimestamp + - transferState + responses: '400': description: Malformed or missing required headers or parameters @@ -833,4 +898,3 @@ components: minLength: 1 maxLength: 128 description: Either a sub-identifier of a `{idValue}`, or a sub-type of the `{idType}`, normally a `{personalIdType}` - diff --git a/pisp-sequences.puml b/pisp-sequences.puml new file mode 100644 index 000000000..66ecddaa2 --- /dev/null +++ b/pisp-sequences.puml @@ -0,0 +1,148 @@ +@startuml +title Mojaloop PISP SDK Interactions +autonumber + +participant "DFSP Backend" as backend +participant "PISP Backend" as pisp + +box "SDK" + participant "SDK Inbound Service" as sib + participant "Cache" as cache + participant "SDK Outbound Service" as sob +end box + +participant "Switch" as sw + + + +note over sib, sob: All outbound between SDK and Switch have\nJWS added automatically +note over sib, sob: All inbound from switch have JWS validated + +note over sib, sob: Mapping between scheme transaction types and DFSP\ntransaction types +note over sib, sob: JWS and TLS keys managed by SDK + +== PISP receives autorization request == + sw -> sib: POST /authorization + sib -> pisp: POST /signedCondition + pisp -> pisp: ask User for authorization + pisp -> sib: in respons body we have signed condition + sib -> sib: prepare response to be send via PUT /authorization/{id} + sib -> sw: PUT /authorization/{id} +== PISP is notified about transaction status == + sw -> sib: PUT /thridPartyRequests/{id} + sib -> cache: cache.publish('thridparty_requests_{id}', response.body) +== PISP is notified about transaction error == + sw -> sib: PUT /thridPartyRequests/{id}/error + sib -> cache: cache.publish('thridparty_requests_error_{id}', response.body) +== DFSP requests authorization == + rnote right of backend #Light + **""POST /authorizations""** + ""FSPIOP-Source: DFSP Backend"" + ""FSPIOP-Destination: PISP"" + { + "authenticationType": "U2F", + "retriesLeft": "1", + "amount": { + "amount": "100", + "currency": "USD" + }, + "transactionId": "987", + **"transactionRequestId": "123"**, + "quote": { + "transferAmount": { + "amount": "100", + "currency": "USD" + }, + "payeeReceiveAmount": { + "amount": "99", + "currency": "USD" + }, + "payeeFspFee": { + "amount": "1", + "currency": "USD" + }, + "expiration": "2020-06-15T12:00:00.000", + "ilpPacket": "...", + "condition": "...", + } + } + end note + backend -> sob: POST /authorizations + sob -> cache: notification_handler = cache.subscribe('authorization/id') + rnote right of sob #Light + **""POST /authorizations""** + ""FSPIOP-Source: DFSP Backend"" + ""FSPIOP-Destination: PISP"" + { + "authenticationType": "U2F", + "retriesLeft": "1", + "amount": { + "amount": "100", + "currency": "USD" + }, + "transactionId": "987", + **"transactionRequestId": "123"**, + "quote": { + "transferAmount": { + "amount": "100", + "currency": "USD" + }, + "payeeReceiveAmount": { + "amount": "99", + "currency": "USD" + }, + "payeeFspFee": { + "amount": "1", + "currency": "USD" + }, + "expiration": "2020-06-15T12:00:00.000", + "ilpPacket": "...", + "condition": "...", + } + } + end note + sob -> sw: POST /authorizations + sw -> sw: request authorization\nfrom PISP's customer\nvia auth-service + rnote left of sw #Light + **""PUT /authorizations/123""** + ""FSPIOP-Source: PISP"" + ""FSPIOP-Destination: DFSP Backend"" + { + "authenticationInfo": { + "authentication": "U2F", + "authenticationValue": { + "pinValue": "", + "counter": "1" + } + } + "responseType": "ENTERED" + } + end note + sw -> sib: PUT /authorizations/**123** + sib -> cache: cache.publish('authorization/{id}', put.body) + cache -> sob: notification('authorization/{id}', put.body) + sob -> sob: notification_handler('authorization/{id}', put.body) + rnote left of sob #Light + **""Response body of POST /authorizations""** + { + "transactionRequestId": "123", + "authenticationInfo": { + "authentication": "U2F", + "authenticationValue": { + "pinValue": "", + "counter": "1" + } + } + "responseType": "ENTERED" + } + end note + sob -> backend: OK authorization details + backend -> backend: verify authorization + alt PROPOSAL: accepting authorization + backend -> sob: OK: PUT /authorizations/**123** + sob -> sw: PUT /authorizations/**123** + else PROPOSAL: not accepting authorization + backend -> sob: NOK: PUT /authorizations/**123**/error + sob -> sw: PUT /authorizations/**123**/error + end +@enduml diff --git a/sequences.puml b/sequences.puml index 832ca2427..0e592322a 100644 --- a/sequences.puml +++ b/sequences.puml @@ -9,10 +9,10 @@ participant "ESB/PortX" as esb box "SDK" participant "SDK Inbound Service" as sib + participant "Cache" as cache participant "SDK Outbound Service" as sob end box - participant "Switch" as sw @@ -94,6 +94,3 @@ note over sib, sob: JWS and TLS keys managed by SDK end - -@enduml - diff --git a/src/.env.example b/src/.env.example index f467c5e99..7eb357797 100644 --- a/src/.env.example +++ b/src/.env.example @@ -11,7 +11,6 @@ TEST_LISTEN_PORT=4002 # environment, i.e. when you're running it locally against your own implementation. INBOUND_MUTUAL_TLS_ENABLED=false OUTBOUND_MUTUAL_TLS_ENABLED=false -TEST_MUTUAL_TLS_ENABLED=false # Enable verification or incoming JWS signatures # Note that signatures will be required on incoming messages @@ -34,6 +33,7 @@ JWS_SIGNING_KEY_PATH=/jwsSigningKey.key JWS_VERIFICATION_KEYS_DIRECTORY=/jwsVerificationKeys # Location of certs and key required for TLS + IN_CA_CERT_PATH=./secrets/cacert.pem IN_SERVER_CERT_PATH=./secrets/servercert.pem IN_SERVER_KEY_PATH=./secrets/serverkey.pem @@ -42,10 +42,6 @@ OUT_CA_CERT_PATH=./secrets/cacert.pem OUT_CLIENT_CERT_PATH=./secrets/servercert.pem OUT_CLIENT_KEY_PATH=./secrets/serverkey.pem -TEST_CA_CERT_PATH=./secrets/cacert.pem -TEST_CLIENT_CERT_PATH=./secrets/servercert.pem -TEST_CLIENT_KEY_PATH=./secrets/serverkey.pem - # The number of space characters by which to indent pretty-printed logs. If set to zero, log events # will each be printed on a single line. LOG_INDENT=0 @@ -55,11 +51,15 @@ CACHE_HOST=172.17.0.2 CACHE_PORT=6379 # SWITCH ENDPOINT -# The option 'PEER_ENDPOINT' has no effect if the remaining three options 'ALS_ENDPOINT', 'QUOTES_ENDPOINT', 'TRANSFERS_ENDPOINT' are specified. +# The option 'PEER_ENDPOINT' has no effect if the remaining options 'ALS_ENDPOINT', 'QUOTES_ENDPOINT', +# 'BULK_QUOTES_ENDPOINT', 'TRANSFERS_ENDPOINT', 'BULK_TRANSFERS_ENDPOINT', 'TRANSACTION_REQUESTS_ENDPOINT' are specified. PEER_ENDPOINT=172.17.0.3:4000 #ALS_ENDPOINT=account-lookup-service.local #QUOTES_ENDPOINT=quoting-service.local +#BULK_QUOTES_ENDPOINT=quoting-service.local #TRANSFERS_ENDPOINT=ml-api-adapter.local +#BULK_TRANSFERS_ENDPOINT=bulk-api-adapter.local +#TRANSACTION_REQUESTS_ENDPOINT=transaction-requests-service.local # BACKEND ENDPOINT BACKEND_ENDPOINT=172.17.0.5:4000 @@ -83,6 +83,10 @@ AUTO_ACCEPT_QUOTES=false # cnofirmation call will be required to progress the transfer to quotes state. AUTO_ACCEPT_PARTY=false +# this flag is for testing purpose only. sdk-scheme-adapter is not supposed to receive PUT /participants/{Type}/{ID}, +# but for testing we can enable it by setting this flag to true +AUTO_ACCEPT_PARTICIPANTS_PUT=false + # when set to true, when sending money via the outbound API, the SDK will use the value # of FSPIOP-Source header from the received quote response as the payeeFsp value in the # transfer prepare request body instead of the value received in the payee party lookup. @@ -128,3 +132,9 @@ ALS_ENDPOINT=127.0.0.1:6500 # The incoming transfer request should consists of an ILP packet and a matching condition in this case. # The fulfilment will be generated from the provided ILP packet, and must hash to the provided condition. ALLOW_TRANSFER_WITHOUT_QUOTE=false + +# To enable request for notification on fulfiled transfer +RESERVE_NOTIFICATION=true + +# resources API versions should be string in format: "resouceOneName=1.0,resourceTwoName=1.1" +RESOURCE_VERSIONS="transfers=1.1,participants=1.1" diff --git a/src/.eslintrc.json b/src/.eslintrc.json index af510af61..00d37f729 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -23,7 +23,7 @@ 2, "always" ], - "no-console": "off", + "no-console": 2, "no-prototype-builtins": "off" } } diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 000000000..63177638d --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,3 @@ +coverage +.eslintcache +.DS_Store \ No newline at end of file diff --git a/src/.npmrc b/src/.npmrc deleted file mode 100644 index 43c97e719..000000000 --- a/src/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/src/InboundServer/api.yaml b/src/InboundServer/api.yaml index e329b3fd9..acce42dea 100644 --- a/src/InboundServer/api.yaml +++ b/src/InboundServer/api.yaml @@ -1,9 +1,9 @@ openapi: 3.0.0 info: - version: '1.0' + version: '1.1' title: Open API for FSP Interoperability (FSPIOP) description: >- - Based on API Definition.docx updated on 2018-03-13 Version 1.0. Note - The + Based on API Definition.docx updated on 2020-05-19 Version 1.1. Note - The API supports a maximum size of 65536 bytes (64 Kilobytes) in the HTTP header. API supports a maximum size of 65536 bytes (64 Kilobytes) in the HTTP header. license: @@ -554,7 +554,7 @@ paths: $ref: '#/components/responses/ErrorResponse501' '503': $ref: '#/components/responses/ErrorResponse503' - /participants: + '/participants': post: description: >- The HTTP request POST /participants is used to create information in the @@ -686,6 +686,13 @@ paths: middleName: Pierre lastName: Trudeau dateOfBirth: '1971-12-25' + accounts: + - currency: USD + description: savings + address: moja.red.8f027046-b82a-4fa9-838b-70210fcf8136 + - currency: USD + description: checkings + address: moja.red.8f027046-b82a-4fa9-838b-70210fcf8137 responses: '200': $ref: '#/components/responses/Response200' @@ -1057,7 +1064,7 @@ paths: schema: $ref: '#/components/schemas/TransactionRequestsIDPutResponse' required: true - /transactionRequests: + '/transactionRequests': post: description: >- The HTTP request POST /transactionRequests is used to request the @@ -1311,7 +1318,7 @@ paths: schema: $ref: '#/components/schemas/QuotesIDPutResponse' required: true - /quotes: + '/quotes': post: description: >- The HTTP request POST /quotes is used to request the creation of a quote @@ -1417,6 +1424,54 @@ paths: schema: $ref: '#/components/schemas/QuotesPostRequest' required: true +#Authorizations + /authorizations: + post: + description: > + The HTTP request `POST /authorizations` is used to request the Payer to enter the + applicable credentials in the PISP system. + summary: Perform PISP authorization + tags: + - authorizations + operationId: AuthorizationsByIDPost + parameters: + #Headers + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Perform authorization + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizationsPostRequest' + responses: + 202: + $ref: '#/components/responses/Response202' + 400: + $ref: '#/components/responses/ErrorResponse400' + 401: + $ref: '#/components/responses/ErrorResponse401' + 403: + $ref: '#/components/responses/ErrorResponse403' + 404: + $ref: '#/components/responses/ErrorResponse404' + 405: + $ref: '#/components/responses/ErrorResponse405' + 406: + $ref: '#/components/responses/ErrorResponse406' + 501: + $ref: '#/components/responses/ErrorResponse501' + 503: + $ref: '#/components/responses/ErrorResponse503' '/authorizations/{ID}': parameters: - $ref: '#/components/parameters/ID' @@ -1488,8 +1543,10 @@ paths: x-examples: application/json: authenticationInfo: - authentication: OTP - authenticationValue: '1234' + authentication: U2F + authenticationValue: + pinValue: '233133331' + counter: '1' responseType: ENTERED responses: '200': @@ -1661,6 +1718,46 @@ paths: schema: $ref: '#/components/schemas/TransfersIDPutResponse' required: true + patch: + description: >- + The HTTP request PATCH /transfers/ is used by a Switch to update the state of a previously reserved transfer, + if the Payee FSP has requested a commit notification when the Switch has completed processing of the transfer. + The in the URI should contain the transferId that was used for the creation of the transfer. + Please note that this request does not generate a callback. + summary: Return transfer information + tags: + - transfers + operationId: TransfersByIDPatch + parameters: + #Headers + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Transfer notification upon completion. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransfersIDPatchResponse' + responses: + 200: + $ref: '#/components/responses/Response200' + 400: + $ref: '#/components/responses/ErrorResponse400' + 401: + $ref: '#/components/responses/ErrorResponse401' + 403: + $ref: '#/components/responses/ErrorResponse403' + 404: + $ref: '#/components/responses/ErrorResponse404' + 405: + $ref: '#/components/responses/ErrorResponse405' + 406: + $ref: '#/components/responses/ErrorResponse406' + 501: + $ref: '#/components/responses/ErrorResponse501' + 503: + $ref: '#/components/responses/ErrorResponse503' + '/transfers/{ID}/error': put: description: >- @@ -1717,7 +1814,7 @@ paths: $ref: '#/components/responses/ErrorResponse503' requestBody: $ref: '#/components/requestBodies/ErrorInformationObject' - /transfers: + '/transfers': post: description: >- The HTTP request POST /transfers is used to request the creation of a @@ -2116,7 +2213,7 @@ paths: schema: $ref: '#/components/schemas/BulkQuotesIDPutResponse' required: true - /bulkQuotes: + '/bulkQuotes': post: description: >- The HTTP request POST /bulkQuotes is used to request the creation of a @@ -2243,8 +2340,8 @@ paths: - $ref: '#/components/parameters/FSPIOP-HTTP-Method' get: description: >- - The HTTP request GET /bulkTransfers/ is used to get information - regarding an earlier created or requested bulk transfer. The in the URI + The HTTP request GET /bulkTransfers/{ID} is used to get information + regarding an earlier created or requested bulk transfer. The ID in the URI should contain the bulkTransferId that was used for the creation of the bulk transfer. summary: BulkTransferByID @@ -2274,10 +2371,10 @@ paths: $ref: '#/components/responses/ErrorResponse503' put: description: >- - The callback PUT /bulkTransfers/ is used to inform the client of a - requested or created bulk transfer. The in the URI should contain the + The callback PUT /bulkTransfers/{ID} is used to inform the client of a + requested or created bulk transfer. The ID in the URI should contain the bulkTransferId that was used for the creation of the bulk transfer (POST - /bulkTransfers), or the that was used in the GET /bulkTransfers/. + /bulkTransfers), or the that was used in the GET /bulkTransfers/{ID}. summary: BulkTransfersByIDPut tags: - bulkTransfers @@ -2354,7 +2451,7 @@ paths: schema: $ref: '#/components/schemas/BulkTransfersIDPutResponse' required: true - /bulkTransfers: + '/bulkTransfers': post: description: >- The HTTP request POST /bulkTransfers is used to request the creation of @@ -2496,6 +2593,108 @@ paths: $ref: '#/components/responses/ErrorResponse503' requestBody: $ref: '#/components/requestBodies/ErrorInformationObject' + #thirdpartyRequests + /thirdpartyRequests/transactions/{ID}: + put: + description: > + The callback `PUT /thirdpartyRequests/transactions/{ID}` is used to inform the client of the result of a + previously-requested transaction. The `{ID}` in the URI should contain the `transactionRequestId` that was used for + the creation of the transaction request. + summary: Update third party transaction requests + tags: + - thirdpartyRequests + operationId: UpdateThirdPartyTransactionRequests + parameters: + #Path + - $ref: '#/components/parameters/ID' + #Headers + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Transaction request result returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ThirdPartyTransactionResponse' + responses: + 200: + $ref: '#/components/responses/Response200' + 400: + $ref: '#/components/responses/ErrorResponse400' + 401: + $ref: '#/components/responses/ErrorResponse401' + 403: + $ref: '#/components/responses/ErrorResponse403' + 404: + $ref: '#/components/responses/ErrorResponse404' + 405: + $ref: '#/components/responses/ErrorResponse405' + 406: + $ref: '#/components/responses/ErrorResponse406' + 501: + $ref: '#/components/responses/ErrorResponse501' + 503: + $ref: '#/components/responses/ErrorResponse503' + /thirdpartyRequests/transactions/{ID}/error: + put: + description: > + If the server is unable to find the transaction request, or another processing error occurs, + the error callback `PUT /thirdpartyRequests/transactions/{ID}/error` is used. + The `{ID}` in the URI should contain the `transactionRequestId` that was used for the creation of the transaction request. + summary: Return transaction error + tags: + - thirdpartyRequests + operationId: UpdateThirdPartyTransactionRequestsError + parameters: + #Path + - $ref: '#/components/parameters/ID' + #Headers + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + 200: + $ref: '#/components/responses/Response200' + 400: + $ref: '#/components/responses/ErrorResponse400' + 401: + $ref: '#/components/responses/ErrorResponse401' + 403: + $ref: '#/components/responses/ErrorResponse403' + 404: + $ref: '#/components/responses/ErrorResponse404' + 405: + $ref: '#/components/responses/ErrorResponse405' + 406: + $ref: '#/components/responses/ErrorResponse406' + 501: + $ref: '#/components/responses/ErrorResponse501' + 503: + $ref: '#/components/responses/ErrorResponse503' + components: parameters: Accept: @@ -2855,17 +3054,48 @@ components: enum: - OTP - QRCODE + - U2F description: >- Below are the allowed values for the enumeration AuthenticationType. - OTP One-time password generated by the Payer FSP. - QRCODE QR code used as One Time Password. AuthenticationValue: title: AuthenticationValue + anyOf: + - $ref: '#/components/schemas/OtpValue' + - $ref: '#/components/schemas/QRCODE' + - $ref: '#/components/schemas/U2FPinValue' + description: Contains the authentication value. The format depends on the authentication type used in the AuthenticationInfo complex type. + U2FPinValue: + title: U2FPinValue + type: object + description: U2F challenge-response, where payer FSP verifies if the response provided by end-user device matches the previously registered key. + properties: + pinValue: + $ref: '#/components/schemas/U2FPIN' + description: U2F challenge-response. + counter: + $ref: '#/components/schemas/Integer' + description: Sequential counter used for cloning detection. Present only for U2F authentication. + required: + - pinValue + - counter + U2FPIN: + title: U2FPIN type: string - pattern: '^\d{3,10}$|^\S{1,64}$' - description: >- - Contains the authentication value. The format depends on the - authentication type used in the AuthenticationInfo complex type. + pattern: ^\S{1,64}$ + minLength: 1 + maxLength: 64 + description: U2F challenge-response, where payer FSP verifies if the response provided by end-user device matches the previously registered key. + Counter: + title: Counter + $ref: '#/components/schemas/Integer' + description: Sequential counter used for cloning detection. Present only for U2F authentication. + RetriesLeft: + title: RetriesLeft + $ref: '#/components/schemas/Integer' + description: RetriesLeft is the number of retries left before the financial transaction is rejected. It must be expressed in the form of the data type Integer. retriesLeft=1 means that this is the last retry before the financial transaction is rejected. + AuthorizationResponse: title: AuthorizationResponse type: string @@ -3104,6 +3334,8 @@ components: - XDR - XOF - XPF + - XTS + - XXX - YER - ZAR - ZMW @@ -3293,6 +3525,8 @@ components: - ACCOUNT_ID - IBAN - ALIAS + - CONSENT + - THIRD_PARTY_LINK description: >- Below are the allowed values for the enumeration - MSISDN An MSISDN (Mobile Station International Subscriber Directory Number, that is, the @@ -3376,6 +3610,7 @@ components: QRCODE: title: QRCODE type: string + pattern: ^\S{1,64}$ minLength: 1 maxLength: 64 description: QR code used as One Time Password. @@ -3514,6 +3749,36 @@ components: required: - authentication - authenticationValue + AuthorizationsPostRequest: + title: AuthorizationsPostRequest + type: object + description: POST /authorizations Request object + properties: + authenticationType: + $ref: '#/components/schemas/AuthenticationType' + description: This value is a valid authentication type from the enumeration AuthenticationType(OTP or QR Code or U2F). + retriesLeft: + $ref: '#/components/schemas/RetriesLeft' + description: RetriesLeft is the number of retries left before the financial transaction is rejected. It must be expressed in the form of the data type Integer. retriesLeft=1 means that this is the last retry before the financial transaction is rejected. + amount: + $ref: '#/components/schemas/Money' + description: This is the transaction amount that will be withdrawn from the Payer’s account. + transactionId: + $ref: '#/components/schemas/CorrelationId' + description: Common ID (decided by the Payer FSP) between the FSPs for the future transaction object. The actual transaction will be created as part of a successful transfer process. + transactionRequestId: + $ref: '#/components/schemas/CorrelationId' + description: The transactionRequestID, received from the POST /transactionRequests service earlier in the process. + quote: + $ref: '#/components/schemas/QuotesIDPutResponse' + description: Quotes object + required: + - authenticationType + - retriesLeft + - amount + - transactionId + - transactionRequestId + - quote AuthorizationsIDPutResponse: title: AuthorizationsIDPutResponse type: object @@ -3894,14 +4159,19 @@ components: ParticipantsTypeIDSubIDPostRequest: title: ParticipantsTypeIDSubIDPostRequest type: object - description: 'POST /participants/{Type}/{ID}/{SubId}, /participants/{Type}/{ID} object' + description: The object sent in the POST /participants/{Type}/{ID}/{SubId} and /participants/{Type}/{ID} requests. An additional optional ExtensionList element has been added as part of v1.1 changes. properties: fspId: $ref: '#/components/schemas/FspId' description: FSP Identifier that the Party belongs to. + example: 1234 currency: $ref: '#/components/schemas/Currency' description: Indicate that the provided Currency is supported by the Party. + example: USD + extensionList: + $ref: '#/components/schemas/ExtensionList' + description: Optional extension, specific to deployment. required: - fspId ParticipantsTypeIDPutResponse: @@ -3965,6 +4235,9 @@ components: type: object description: Data model for the complex type Party. properties: + accounts: + $ref: '#/components/schemas/AccountList' + description: List of accounts associated with the party containing and DFSP routable address, currency identifier and description. partyIdInfo: $ref: '#/components/schemas/PartyIdInfo' description: 'Party Id type, id, sub ID or type, and FSP Id.' @@ -4000,23 +4273,27 @@ components: PartyIdInfo: title: PartyIdInfo type: object - description: Data model for the complex type PartyIdInfo. + description: Data model for the complex type PartyIdInfo. An ExtensionList element has been added to this reqeust in version v1.1 properties: partyIdType: $ref: '#/components/schemas/PartyIdType' description: Type of the identifier. + example: PERSONAL_ID partyIdentifier: $ref: '#/components/schemas/PartyIdentifier' description: An identifier for the Party. + example: 16135551212 partySubIdOrType: $ref: '#/components/schemas/PartySubIdOrType' description: A sub-identifier or sub-type for the Party. + example: DRIVING_LICENSE fspId: $ref: '#/components/schemas/FspId' - description: FSP ID (if known) + description: FSP ID (if known). + example: 1234 extensionList: $ref: '#/components/schemas/ExtensionList' - description: 'Optional extension, specific to deployment.' + description: Optional extension, specific to deployment. required: - partyIdType - partyIdentifier @@ -4424,4 +4701,82 @@ components: description: 'Optional extension, specific to deployment.' required: - transferState - + ThirdPartyTransactionResponse: + title: ThirdPartyTransactionResponse + type: object + description: The object sent in the PUT /thirdpartyRequests/transactions/{ID} request. + properties: + transactionId: + $ref: '#/components/schemas/CorrelationId' + description: > + Identifies a related transaction (if a transaction has been created) + transactionRequestState: + $ref: '#/components/schemas/TransactionRequestState' + description: State of the transaction request` + AccountId: + type: string + description: > + A long-lived account identifier provided by the DFSP + this MUST NOT be Bank Account Number or anything that + may expose a User's private bank account information + AccountList: + title: AccountList + type: object + description: Data model for the complex type AccountList + properties: + account: + type: array + items: + $ref: '#/components/schemas/Account' + minItems: 1 + maxItems: 32 + description: Accounts associated with the Party + required: + - account + Account: + title: Account + type: object + description: Data model for the complex type Account + properties: + address: + $ref: '#/components/schemas/AccountAddress' + type: string + description: Unique routable address which is DFSP specific. + currency: + $ref: '#/components/schemas/Currency' + type: string + description: Currency of the amount. + description: + $ref: '#/components/schemas/Name' + type: string + description: The name of the account. + required: + - address + - currency + - description + AccountAddress: + title: AccountAddress + type: string + description: Unique routable address which is DFSP specific. + pattern: ^([0-9A-Za-z_~\-\.]+[0-9A-Za-z_~\-])$ + minLength: 1 + maxLength: 1023 + TransfersIDPatchResponse: + title: TransfersIDPatchResponse + type: object + description: PATCH /transfers/{ID} object + properties: + completedTimestamp: + $ref: '#/components/schemas/DateTime' + description: Time and date when the transaction was completed. + example: "2020-05-19T08:38:08.699-04:00" + transferState: + $ref: '#/components/schemas/TransferState' + description: State of the transfer. + example: RESERVED + extensionList: + $ref: '#/components/schemas/ExtensionList' + description: Optional extension, specific to deployment. + required: + - completedTimestamp + - transferState diff --git a/src/InboundServer/handlers.js b/src/InboundServer/handlers.js index 4e05276d4..64fffbd37 100644 --- a/src/InboundServer/handlers.js +++ b/src/InboundServer/handlers.js @@ -6,12 +6,19 @@ * * * ORIGINAL AUTHOR: * * James Bush - james.bush@modusbox.com * + * CONTRIBUTORS: * + * Steven Oderayi - steven.oderayi@modusbox.com * + * Paweł Marzec - pawel.marzec@modusbox.com * + * Sridhar Voruganti - sridhar.voruganti@modusbox.com * **************************************************************************/ 'use strict'; -const util = require('util'); const Model = require('@internal/model').InboundTransfersModel; +const AuthorizationsModel = require('@internal/model').OutboundAuthorizationsModel; +const ThirdpartyTrxnModelIn = require('@internal/model').InboundThirdpartyTransactionModel; +const ThirdpartyTrxnModelOut = require('@internal/model').OutboundThirdpartyTransactionModel; +const PartiesModel = require('@internal/model').PartiesModel; /** * Handles a GET /authorizations/{id} request @@ -20,21 +27,13 @@ const getAuthorizationsById = async (ctx) => { // kick off an asyncronous operation to handle the request (async () => { try { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers - }; - const res = await ctx.state.cache.set(`request_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing request : ${util.inspect(res)}`); - } - // use the transfers model to execute asynchronous stages with the switch const model = new Model({ ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -57,6 +56,42 @@ const getAuthorizationsById = async (ctx) => { ctx.response.body = ''; }; +/** + * Handles POST /authorizations request + */ +const postAuthorizations = async (ctx) => { + // kick off an asyncronous operation to handle the request + (async () => { + try { + // use the transfers model to execute asynchronous stages with the switch + const thirdpartyTrxnModelIn = new ThirdpartyTrxnModelIn({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + const sourceFspId = ctx.request.headers['fspiop-source']; + + // use the model to handle the request + const response = await thirdpartyTrxnModelIn.postAuthorizations(ctx.request.body, sourceFspId); + + // log the result + ctx.state.logger.push({ response }).log('Inbound Third party transaction model handled POST /authorizations request'); + } + catch (err) { + // nothing we can do if an error gets thrown back to us here apart from log it and continue + ctx.state.logger.push({ err }).log('Error handling POST /authorizations}'); + } + })(); + + // Note that we will have passed request validation, JWS etc... by this point + // so it is safe to return 202 + ctx.response.status = 202; + ctx.response.body = ''; +}; + /** * Handles a GET /participants/{idType}/{idValue} request */ @@ -69,7 +104,8 @@ const getParticipantsByTypeAndId = async (ctx) => { ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -101,21 +137,13 @@ const getPartiesByTypeAndId = async (ctx) => { // kick off an asyncronous operation to handle the request (async () => { try { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers - }; - const res = await ctx.state.cache.set(`request_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing request : ${util.inspect(res)}`); - } - // use the transfers model to execute asynchronous stages with the switch const model = new Model({ ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -157,22 +185,13 @@ const postQuotes = async (ctx) => { // kick off an asyncronous operation to handle the request (async () => { try { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`request_${ctx.request.body.quoteId}`, req); - ctx.state.logger.log(`Cacheing request: ${util.inspect(res)}`); - } - // use the transfers model to execute asynchronous stages with the switch const model = new Model({ ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -203,22 +222,13 @@ const postTransfers = async (ctx) => { // kick off an asyncronous operation to handle the request (async () => { try { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`request_${ctx.request.body.transferId}`, req); - ctx.state.logger.log(`Cacheing request: ${util.inspect(res)}`); - } - // use the transfers model to execute asynchronous stages with the switch const model = new Model({ ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -248,22 +258,13 @@ const getTransfersById = async (ctx) => { // kick off an asyncronous operation to handle the request (async () => { try { - if (ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers - }; - const res = await ctx.state.cache.set( - `request_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Caching request : ${util.inspect(res)}`); - } - // use the transfers model to execute asynchronous stages with the switch const model = new Model({ ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -295,22 +296,13 @@ const postTransactionRequests = async (ctx) => { // kick off an asyncronous operation to handle the request (async () => { try { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`request_${ctx.request.body.transactionRequestId}`, req); - ctx.state.logger.log(`Cacheing request: ${util.inspect(res)}`); - } - // use the transfers model to execute asynchronous stages with the switch const model = new Model({ ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, }); const sourceFspId = ctx.request.headers['fspiop-source']; @@ -334,30 +326,22 @@ const postTransactionRequests = async (ctx) => { }; /** - * Handles a PUT /authorizations/{id}. This is a response to a GET /authorizations/{ID} + * Handles a PUT /authorizations/{id}. This is a response to a GET /authorizations/{ID} or POST /authorizations/{ID} * request. */ const putAuthorizationsById = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing request: ${util.inspect(res)}`); - } - const idValue = ctx.state.path.params.ID; - - // publish an event onto the cache for subscribers to action - const cacheId = `otp_${idValue}`; - // publish an event onto the cache for subscribers to action - await ctx.state.cache.publish(cacheId, { + + const authorizationChannel = ctx.state.conf.enablePISPMode + ? AuthorizationsModel.notificationChannel(idValue) + : `otp_${idValue}`; + + await ctx.state.cache.publish(authorizationChannel, { type: 'authorizationsResponse', data: ctx.request.body, headers: ctx.request.headers }); + ctx.response.status = 200; }; @@ -365,16 +349,6 @@ const putAuthorizationsById = async (ctx) => { * Handles a PUT /participants/{ID}. This is a response to a POST /participants request */ const putParticipantsById = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Caching callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`ac_${ctx.state.path.params.ID}`, { type: 'accountsCreationSuccessfulResponse', @@ -389,16 +363,6 @@ const putParticipantsById = async (ctx) => { * Handles a PUT /participants/{ID}/error. This is an error response to a POST /participants request */ const putParticipantsByIdError = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Caching callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`ac_${ctx.state.path.params.ID}`, { type: 'accountsCreationErrorResponse', @@ -414,8 +378,40 @@ const putParticipantsByIdError = async (ctx) => { * Handles a PUT /participants/{idType}/{idValue} request */ const putParticipantsByTypeAndId = async (ctx) => { - // SDK does not make participants requests so we should not expect any calls to this method - ctx.response.status = 501; + // Allow putParticipants only for testing purpose when `AUTO_ACCEPT_PARTICIPANTS_PUT` env variable is set to true. + if(ctx.state.conf.autoAcceptParticipantsPut){ + const idType = ctx.state.path.params.Type; + const idValue = ctx.state.path.params.ID; + const idSubValue = ctx.state.path.params.SubId; + + // publish an event onto the cache for subscribers to action + const cacheId = `${idType}_${idValue}` + (idSubValue ? `_${idSubValue}` : ''); + await ctx.state.cache.publish(cacheId, ctx.request.body); + ctx.response.status = 200; + } else { + // SDK does not make participants requests so we should not expect any calls to this method + ctx.response.status = 501; + ctx.response.body = ''; + } +}; + + +/** + * Handles a PUT /participants/{Type}/{ID}/{SubId}/error request. This is an error response to a GET /participants/{Type}/{ID}/{SubId} request + */ +const putParticipantsByTypeAndIdError = async(ctx) => { + const idType = ctx.state.path.params.Type; + const idValue = ctx.state.path.params.ID; + const idSubValue = ctx.state.path.params.SubId; + + // publish an event onto the cache for subscribers to action + // note that we publish the event the same way we publish a success PUT + // the subscriber will notice the body contains an errorInformation property + // and recognise it as an error response + const cacheId = `${idType}_${idValue}` + (idSubValue ? `_${idSubValue}` : ''); + await ctx.state.cache.publish(cacheId, ctx.request.body); + + ctx.response.status = 200; ctx.response.body = ''; }; @@ -425,23 +421,18 @@ const putParticipantsByTypeAndId = async (ctx) => { * request. */ const putPartiesByTypeAndId = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing request: ${util.inspect(res)}`); - } - const idType = ctx.state.path.params.Type; const idValue = ctx.state.path.params.ID; const idSubValue = ctx.state.path.params.SubId; - // publish an event onto the cache for subscribers to action + // generate keys const cacheId = `${idType}_${idValue}` + (idSubValue ? `_${idSubValue}` : ''); + const channelName = PartiesModel.channelName(idType, idValue, idSubValue); + + // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(cacheId, ctx.request.body); + await ctx.state.cache.publish(channelName, ctx.request.body); + ctx.response.status = 200; }; @@ -450,16 +441,6 @@ const putPartiesByTypeAndId = async (ctx) => { * Handles a PUT /quotes/{ID}. This is a response to a POST /quotes request */ const putQuoteById = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`qt_${ctx.state.path.params.ID}`, { type: 'quoteResponse', @@ -470,20 +451,46 @@ const putQuoteById = async (ctx) => { ctx.response.status = 200; }; +/** + * Handles a GET /quotes/{ID} + */ +const getQuoteById = async (ctx) => { + // kick off an asyncronous operation to handle the request + (async () => { + try { + // use the transfers model to execute asynchronous stages with the switch + const model = new Model({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + const sourceFspId = ctx.request.headers['fspiop-source']; + + // use the model to handle the request + const response = await model.getQuoteRequest(ctx.state.path.params.ID, sourceFspId); + + // log the result + ctx.state.logger.push({ response }).log('Inbound transfers model handled GET /quotes request'); + } + catch(err) { + // nothing we can do if an error gets thrown back to us here apart from log it and continue + ctx.state.logger.push({ err }).log('Error handling GET /quotes'); + } + })(); + + // Note that we will have passed request validation, JWS etc... by this point + // so it is safe to return 200 + ctx.response.status = 200; + +}; + /** * Handles a PUT /quotes/{ID}. This is a response to a POST /quotes request */ const putTransactionRequestsById = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`txnreq_${ctx.state.path.params.ID}`, { type: 'transactionRequestResponse', @@ -495,19 +502,9 @@ const putTransactionRequestsById = async (ctx) => { }; /** - * Handles a PUT /transfers/{ID}. This is a response to a POST|GET /transfers request + * Handles a PUT /transfers/{ID}. This is a response to a POST|PATCH|GET /transfers request */ const putTransfersById = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Caching callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`tf_${ctx.state.path.params.ID}`, { type: 'transferFulfil', @@ -517,21 +514,38 @@ const putTransfersById = async (ctx) => { ctx.response.status = 200; }; +/** + * Handles a PATCH /transfers/{ID} from the Switch to Payee for successful transfer + */ +const patchTransfersById = async (ctx) => { + const req = { + headers: ctx.request.headers, + data: ctx.request.body + }; + + const idValue = ctx.state.path.params.ID; + + // use the transfers model to execute asynchronous stages with the switch + const model = new Model({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + // sends notification to the payee fsp + const response = await model.sendNotificationToPayee(req.data, idValue); + + // log the result + ctx.state.logger.push({response}). + log('Inbound transfers model handled PATCH /transfers/{ID} request'); +}; /** * Handles a PUT /parties/{Type}/{ID}/error request. This is an error response to a GET /parties/{Type}/{ID} request */ const putPartiesByTypeAndIdError = async(ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing request: ${util.inspect(res)}`); - } - const idType = ctx.state.path.params.Type; const idValue = ctx.state.path.params.ID; const idSubValue = ctx.state.path.params.SubId; @@ -540,9 +554,15 @@ const putPartiesByTypeAndIdError = async(ctx) => { // note that we publish the event the same way we publish a success PUT // the subscriber will notice the body contains an errorInformation property // and recognise it as an error response + // generate keys const cacheId = `${idType}_${idValue}` + (idSubValue ? `_${idSubValue}` : ''); - await ctx.state.cache.publish(cacheId, ctx.request.body); + const channelName = PartiesModel.channelName(idType, idValue, idSubValue); + // publish an event onto the cache for subscribers to action + await Promise.all([ + ctx.state.cache.publish(cacheId, ctx.request.body), + ctx.state.cache.publish(channelName, ctx.request.body) + ]); ctx.response.status = 200; ctx.response.body = ''; }; @@ -552,16 +572,6 @@ const putPartiesByTypeAndIdError = async(ctx) => { * Handles a PUT /quotes/{ID}/error request. This is an error response to a POST /quotes request */ const putQuotesByIdError = async(ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`qt_${ctx.state.path.params.ID}`, { type: 'quoteResponseError', @@ -577,16 +587,6 @@ const putQuotesByIdError = async(ctx) => { * Handles a PUT /transfers/{ID}/error. This is an error response to a POST /transfers request */ const putTransfersByIdError = async (ctx) => { - if(ctx.state.conf.enableTestFeatures) { - // we are in test mode so cache the request - const req = { - headers: ctx.request.headers, - data: ctx.request.body - }; - const res = await ctx.state.cache.set(`callback_${ctx.state.path.params.ID}`, req); - ctx.state.logger.log(`Cacheing callback: ${util.inspect(res)}`); - } - // publish an event onto the cache for subscribers to action await ctx.state.cache.publish(`tf_${ctx.state.path.params.ID}`, { type: 'transferError', @@ -597,21 +597,270 @@ const putTransfersByIdError = async (ctx) => { ctx.response.body = ''; }; +/** + * Handles a GET /bulkQuotes/{ID} request + */ +const getBulkQuotesById = async (ctx) => { + // kick off an asyncronous operation to handle the request + (async () => { + try { + // use the transfers model to execute asynchronous stages with the switch + const model = new Model({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + const sourceFspId = ctx.request.headers['fspiop-source']; + + // use the model to handle the request + const response = await model.getBulkQuote(ctx.state.path.params.ID, + sourceFspId); + + // log the result + ctx.state.logger.push({response}). + log('Inbound transfers model handled GET /bulkQuotes/{ID} request'); + } + catch(err) { + // nothing we can do if an error gets thrown back to us here apart from log it and continue + ctx.state.logger.push({ err }).log('Error handling GET /bulkQuotes/{ID}'); + } + })(); + + // Note that we will have passed request validation, JWS etc... by this point + // so it is safe to return 202 + ctx.response.status = 202; + ctx.response.body = ''; +}; + +/** + * Handles a POST /bulkQuotes request + */ +const postBulkQuotes = async (ctx) => { + (async () => { + try { + // use the transfers model to execute asynchronous stages with the switch + const model = new Model({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + const sourceFspId = ctx.request.headers['fspiop-source']; + + // use the model to handle the request + const response = await model.bulkQuoteRequest(ctx.request.body, sourceFspId); + + // log the result + ctx.state.logger.push({ response }).log('Inbound transfers model handled POST /bulkQuotes request'); + } + catch(err) { + // nothing we can do if an error gets thrown back to us here apart from log it and continue + ctx.state.logger.push({ err }).log('Error handling POST /bulkQuotes'); + } + })(); + + ctx.response.status = 202; + ctx.response.body = ''; +}; + +/** + * Handles a PUT /bulkQuotes/{ID}. This is a response to a POST /bulkQuotes request + */ +const putBulkQuotesById = async (ctx) => { + // publish an event onto the cache for subscribers to action + await ctx.state.cache.publish(`bulkQuotes_${ctx.state.path.params.ID}`, { + type: 'bulkQuoteResponse', + data: ctx.request.body, + headers: ctx.request.headers + }); + + ctx.response.status = 200; +}; + +/** + * Handles a PUT /bulkQuotes/{ID}/error request. This is an error response to a POST /bulkQuotes request + */ +const putBulkQuotesByIdError = async(ctx) => { + // publish an event onto the cache for subscribers to action + await ctx.state.cache.publish(`bulkQuotes_${ctx.state.path.params.ID}`, { + type: 'bulkQuoteResponseError', + data: ctx.request.body + }); + + ctx.response.status = 200; + ctx.response.body = ''; +}; + +/** + * Handles a GET /bulkTransfers/{ID} request + */ +const getBulkTransfersById = async (ctx) => { + // kick off an asyncronous operation to handle the request + (async () => { + try { + // use the transfers model to execute asynchronous stages with the switch + const model = new Model({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + const sourceFspId = ctx.request.headers['fspiop-source']; + + // use the model to handle the request + const response = await model.getBulkTransfer(ctx.state.path.params.ID, + sourceFspId); + + // log the result + ctx.state.logger.push({response}). + log('Inbound transfers model handled GET /bulkTransfers/{ID} request'); + } + catch(err) { + // nothing we can do if an error gets thrown back to us here apart from log it and continue + ctx.state.logger.push({ err }).log('Error handling GET /bulkTransfers/{ID}'); + } + })(); + + // Note that we will have passed request validation, JWS etc... by this point + // so it is safe to return 202 + ctx.response.status = 202; + ctx.response.body = ''; +}; + +/** + * Handles a POST /bulkTransfers request + */ +const postBulkTransfers = async (ctx) => { + (async () => { + try { + // use the transfers model to execute asynchronous stages with the switch + const model = new Model({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + resourceVersions: ctx.resourceVersions, + }); + + const sourceFspId = ctx.request.headers['fspiop-source']; + + // use the model to handle the request + const response = await model.prepareBulkTransfer(ctx.request.body, sourceFspId); + + // log the result + ctx.state.logger.push({ response }).log('Inbound transfers model handled POST /bulkTransfers request'); + } + catch(err) { + // nothing we can do if an error gets thrown back to us here apart from log it and continue + ctx.state.logger.push({ err }).log('Error handling POST /bulkTransfers'); + } + })(); + + ctx.response.status = 202; + ctx.response.body = ''; +}; + +/** + * Handles a PUT /bulkTransfers/{ID}. This is a response to a POST /bulkTransfers request + */ +const putBulkTransfersById = async (ctx) => { + // publish an event onto the cache for subscribers to action + await ctx.state.cache.publish(`bulkTransfer_${ctx.state.path.params.ID}`, { + type: 'bulkTransferResponse', + data: ctx.request.body, + headers: ctx.request.headers + }); + + ctx.response.status = 200; +}; + +/** + * Handles a PUT /bulkTransfers/{ID}/error request. This is an error response to a POST /bulkTransfers request + */ +const putBulkTransfersByIdError = async(ctx) => { + // publish an event onto the cache for subscribers to action + await ctx.state.cache.publish(`bulkTransfer_${ctx.state.path.params.ID}`, { + type: 'bulkTransferResponseError', + data: ctx.request.body + }); -const healthCheck = async(ctx) => { ctx.response.status = 200; ctx.response.body = ''; }; +/** + * Handles PUT /thirdpartyRequests/transactions/{ID} request. + * This is response to a POST /thirdpartyRequests/transactions request + */ +const putThirdPartyReqTransactionsById = async (ctx) => { + // publish an event onto the cache for subscribers to action + await ThirdpartyTrxnModelOut.publishNotifications(ctx.state.cache, ctx.state.path.params.ID, { + type: 'thirdpartyTransactionsReqResponse', + data: ctx.request.body, + headers: ctx.request.headers + }); + + ctx.response.status = 200; +}; + +/** + * Handles PUT /thirdpartyRequests/transactions/{ID}/error. + * This is error response to POST /thirdpartyRequests/transactions request + */ +const putThirdPartyReqTransactionsByIdError = async (ctx) => { + // publish an event onto the cache for subscribers to action + await ThirdpartyTrxnModelOut.publishNotifications(ctx.state.cache, ctx.state.path.params.ID, { + type: 'thirdpartyTransactionsReqErrorResponse', + data: ctx.request.body, + headers: ctx.request.headers + }); + + ctx.response.status = 200; +}; + +const healthCheck = async(ctx) => { + ctx.response.status = 200; + ctx.response.body = ''; +}; module.exports = { '/': { get: healthCheck }, + '/authorizations': { + post: postAuthorizations + }, '/authorizations/{ID}': { get: getAuthorizationsById, put: putAuthorizationsById }, + '/bulkQuotes': { + post: postBulkQuotes + }, + '/bulkQuotes/{ID}': { + get: getBulkQuotesById, + put: putBulkQuotesById + }, + '/bulkQuotes/{ID}/error': { + put: putBulkQuotesByIdError + }, + '/bulkTransfers': { + post: postBulkTransfers + }, + '/bulkTransfers/{ID}': { + get: getBulkTransfersById, + put: putBulkTransfersById + }, + '/bulkTransfers/{ID}/error': { + put: putBulkTransfersByIdError + }, '/participants/{ID}': { put: putParticipantsById }, @@ -619,10 +868,13 @@ module.exports = { put: putParticipantsByTypeAndId, get: getParticipantsByTypeAndId }, - '/participants/{Type}/{SubId}/{ID}': { + '/participants/{Type}/{ID}/{SubId}': { put: putParticipantsByTypeAndId, get: getParticipantsByTypeAndId }, + '/participants/{Type}/{ID}/{SubId}/error': { + put: putParticipantsByTypeAndIdError + }, '/participants/{ID}/error': { put: putParticipantsByIdError }, @@ -646,7 +898,8 @@ module.exports = { post: postQuotes }, '/quotes/{ID}': { - put: putQuoteById + put: putQuoteById, + get: getQuoteById }, '/quotes/{ID}/error': { put: putQuotesByIdError @@ -656,7 +909,8 @@ module.exports = { }, '/transfers/{ID}': { get: getTransfersById, - put: putTransfersById + put: putTransfersById, + patch: patchTransfersById }, '/transfers/{ID}/error': { put: putTransfersByIdError @@ -666,5 +920,11 @@ module.exports = { }, '/transactionRequests/{ID}': { put: putTransactionRequestsById + }, + '/thirdpartyRequests/transactions/{ID}': { + put: putThirdPartyReqTransactionsById + }, + '/thirdpartyRequests/transactions/{ID}/error': { + put: putThirdPartyReqTransactionsByIdError } }; diff --git a/src/InboundServer/index.js b/src/InboundServer/index.js index c6c06c90b..0df576eb1 100644 --- a/src/InboundServer/index.js +++ b/src/InboundServer/index.js @@ -10,152 +10,68 @@ const Koa = require('koa'); +const assert = require('assert').strict; const https = require('https'); const http = require('http'); const yaml = require('js-yaml'); const fs = require('fs'); const path = require('path'); +const EventEmitter = require('events'); const { WSO2Auth } = require('@mojaloop/sdk-standard-components'); -const { Logger, Transports } = require('@internal/log'); -const Cache = require('@internal/cache'); const Validate = require('@internal/validate'); const router = require('@internal/router'); const handlers = require('./handlers'); const middlewares = require('./middlewares'); -class InboundServer { - constructor(conf) { +class InboundApi extends EventEmitter { + constructor(conf, logger, cache, validator) { + super({ captureExceptions: true }); this._conf = conf; - this._api = null; - this._server = null; - this._logger = null; - this._jwsVerificationKeys = {}; - } - - async setupApi() { - this._api = new Koa(); - this._logger = await this._createLogger(); - - this._cache = await this._createCache(); - - const specPath = path.join(__dirname, 'api.yaml'); - const apiSpecs = yaml.load(fs.readFileSync(specPath)); - const validator = new Validate(); - await validator.initialise(apiSpecs); - - this._wso2Auth = new WSO2Auth({ - ...this._conf.wso2Auth, - logger: this._logger, - tlsCreds: this._conf.tls.outbound.mutualTLS.enabled && this._conf.tls.outbound.creds, + this._cache = cache; + this._wso2 = { + auth: new WSO2Auth({ + ...conf.wso2.auth, + logger, + tlsCreds: conf.outbound.tls.mutualTLS.enabled && conf.outbound.tls.creds, + }), + retryWso2AuthFailureTimes: conf.wso2.requestAuthFailureRetryTimes, + }; + this._wso2.auth.on('error', (msg) => { + this.emit('error', 'WSO2 auth error in InboundApi', msg); }); - this._api.use(middlewares.createErrorHandler()); - this._api.use(middlewares.createRequestIdGenerator()); - this._api.use(middlewares.createHeaderValidator(this._logger)); - - if(this._conf.validateInboundJws) { - const jwsExclusions = []; - if (!this._conf.validateInboundPutPartiesJws) { - jwsExclusions.push('putParties'); - } - this._jwsVerificationKeys = this._getJwsKeys(); - this._api.use(middlewares.createJwsValidator(this._logger, this._jwsVerificationKeys, jwsExclusions)); + if (conf.validateInboundJws) { + this._jwsVerificationKeys = InboundApi._GetJwsKeys(conf.jwsVerificationKeysDirectory); } - - const sharedState = { cache: this._cache, wso2Auth: this._wso2Auth, conf: this._conf }; - this._api.use(middlewares.createLogger(this._logger, sharedState)); - - this._api.use(middlewares.createRequestValidator(validator)); - this._api.use(router(handlers)); - this._api.use(middlewares.createResponseBodyHandler()); - - this._server = this._createServer(); - return this._server; + this._api = InboundApi._SetupApi({ + conf, + logger, + validator, + cache, + jwsVerificationKeys: this._jwsVerificationKeys, + wso2: this._wso2, + }); } async start() { this._startJwsWatcher(); - await this._cache.connect(); if (!this._conf.testingDisableWSO2AuthStart) { - await this._wso2Auth.start(); - } - if (!this._conf.testingDisableServerStart) { - await new Promise((resolve) => this._server.listen(this._conf.inboundPort, resolve)); - this._logger.log(`Serving inbound API on port ${this._conf.inboundPort}`); + await this._wso2.auth.start(); } } - async stop() { - if (!this._server) { - return; + stop() { + this._wso2.auth.stop(); + if (this._keyWatcher) { + this._keyWatcher.close(); + this._keyWatcher = null; } - await new Promise(resolve => this._server.close(resolve)); - this._wso2Auth.stop(); - await this._cache.disconnect(); - this._stopJwsWatcher(); - console.log('inbound shut down complete'); - } - - async _createLogger() { - const transports = await Promise.all([Transports.consoleDir()]); - // Set up a logger for each running server - return new Logger({ - context: { - app: 'mojaloop-sdk-inbound-api' - }, - space: this._conf.logIndent, - transports, - }); } - async _createCache() { - const transports = await Promise.all([Transports.consoleDir()]); - const logger = new Logger({ - context: { - app: 'mojaloop-sdk-inboundCache' - }, - space: this._conf.logIndent, - transports, - }); - - const cacheConfig = { - ...this._conf.cacheConfig, - logger - }; - - return new Cache(cacheConfig); - } - - _createServer() { - let server; - // If config specifies TLS, start an HTTPS server; otherwise HTTP - if (this._conf.tls.inbound.mutualTLS.enabled) { - const inboundHttpsOpts = { - ...this._conf.tls.inbound.creds, - requestCert: true, - rejectUnauthorized: true // no effect if requestCert is not true - }; - server = https.createServer(inboundHttpsOpts, this._api.callback()); - } else { - server = http.createServer(this._api.callback()); - } - return server; - } - - _getJwsKeys() { - const keys = {}; - if (this._conf.jwsVerificationKeysDirectory) { - fs.readdirSync(this._conf.jwsVerificationKeysDirectory) - .filter(f => f.endsWith('.pem')) - .forEach(f => { - const keyName = path.basename(f, '.pem'); - const keyPath = path.join(this._conf.jwsVerificationKeysDirectory, f); - keys[keyName] = fs.readFileSync(keyPath); - }); - } - return keys; + callback() { + return this._api.callback(); } _startJwsWatcher() { @@ -182,15 +98,108 @@ class InboundServer { } }; if (this._conf.jwsVerificationKeysDirectory) { - this.keyWatcher = fs.watch(this._conf.jwsVerificationKeysDirectory, watchHandler); + this._keyWatcher = fs.watch(this._conf.jwsVerificationKeysDirectory, watchHandler); } } - _stopJwsWatcher() { - if (this.keyWatcher) { - this.keyWatcher.close(); + static _SetupApi({ conf, logger, validator, cache, jwsVerificationKeys, wso2 }) { + const api = new Koa(); + + api.use(middlewares.createErrorHandler(logger)); + api.use(middlewares.createRequestIdGenerator()); + api.use(middlewares.createHeaderValidator(logger)); + if (conf.validateInboundJws) { + const jwsExclusions = conf.validateInboundPutPartiesJws ? [] : ['putParties']; + api.use(middlewares.createJwsValidator(logger, jwsVerificationKeys, jwsExclusions)); + } + + api.use(middlewares.applyState({ cache, wso2, conf })); + api.use(middlewares.createLogger(logger)); + api.use(middlewares.createRequestValidator(validator)); + api.use(middlewares.assignFspiopIdentifier()); + if (conf.enableTestFeatures) { + api.use(middlewares.cacheRequest(cache)); + } + api.use(router(handlers)); + api.use(middlewares.createResponseBodyHandler()); + + api.context.resourceVersions = conf.resourceVersions; + + return api; + } + + static _GetJwsKeys(fromDir) { + const keys = {}; + if (fromDir) { + fs.readdirSync(fromDir) + .filter(f => f.endsWith('.pem')) + .forEach(f => { + const keyName = path.basename(f, '.pem'); + const keyPath = path.join(fromDir, f); + keys[keyName] = fs.readFileSync(keyPath); + }); + } + return keys; + } +} + +class InboundServer extends EventEmitter { + constructor(conf, logger, cache) { + super({ captureExceptions: true }); + this._conf = conf; + this._validator = new Validate(); + this._logger = logger; + this._api = new InboundApi( + conf, + this._logger.push({ component: 'api' }), + cache, + this._validator + ); + this._api.on('error', (...args) => { + this.emit('error', ...args); + }); + this._server = this._createServer( + conf.inbound.tls.mutualTLS.enabled, + conf.inbound.tls.creds, + this._api.callback() + ); + } + + async start() { + assert(!this._server.listening, 'Server already listening'); + const specPath = path.join(__dirname, 'api.yaml'); + const apiSpecs = yaml.load(fs.readFileSync(specPath)); + await this._validator.initialise(apiSpecs); + await this._api.start(); + await new Promise((resolve) => this._server.listen(this._conf.inbound.port, resolve)); + this._logger.log(`Serving inbound API on port ${this._conf.inbound.port}`); + } + + async stop() { + if (this._server) { + await new Promise(resolve => this._server.close(resolve)); + this._server = null; + } + if (this._api) { + await this._api.stop(); + this._api = null; } + this._logger.log('inbound shut down complete'); } + + _createServer(tlsEnabled, tlsCreds, handler) { + if (!tlsEnabled) { + return http.createServer(handler); + } + + const inboundHttpsOpts = { + ...tlsCreds, + requestCert: true, + rejectUnauthorized: true // no effect if requestCert is not true + }; + return https.createServer(inboundHttpsOpts, handler); + } + } module.exports = InboundServer; diff --git a/src/InboundServer/middlewares.js b/src/InboundServer/middlewares.js index 1669ba142..77f57da3f 100644 --- a/src/InboundServer/middlewares.js +++ b/src/InboundServer/middlewares.js @@ -8,7 +8,6 @@ * James Bush - james.bush@modusbox.com * **************************************************************************/ -const util = require('util'); const coBody = require('co-body'); const randomPhrase = require('@internal/randomphrase'); @@ -18,16 +17,131 @@ const { Jws, Errors } = require('@mojaloop/sdk-standard-components'); * Log raw to console as a last resort * @return {Function} */ -const createErrorHandler = () => async (ctx, next) => { +const createErrorHandler = (logger) => async (ctx, next) => { try { await next(); } catch (err) { // TODO: return a 500 here if the response has not already been sent? - console.log(`Error caught in catchall: ${err.stack || util.inspect(err, { depth: 10 })}`); + logger.push({ err }).log('Error caught in catchall'); } }; +/** + * tag each incoming request with the FSPIOP identifier from it's path or body + * @return {Function} + */ +const assignFspiopIdentifier = () => async (ctx, next) => { + const getters = { + '/authorizations/{ID}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + }, + '/bulkQuotes': { + post: () => ctx.request.body.bulkQuoteId, + }, + '/bulkQuotes/{ID}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + }, + '/bulkQuotes/{ID}/error': { + put: () => ctx.state.path.params.ID, + }, + '/bulkTransfers': { + post: () => ctx.request.body.bulkTransferId, + }, + '/bulkTransfers/{ID}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + }, + '/bulkTransfers/{ID}/error': { + put: () => ctx.state.path.params.ID, + }, + '/participants/{ID}': { + put: () => ctx.state.path.params.ID, + }, + '/participants/{Type}/{ID}': { + get: () => ctx.state.path.params.ID, + }, + '/participants/{Type}/{ID}/{SubId}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + }, + '/participants/{Type}/{ID}/{SubId}/error': { + put: () => ctx.state.path.params.ID, + }, + '/participants/{ID}/error': { + put: () => ctx.state.path.params.ID, + }, + '/parties/{Type}/{ID}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + }, + '/parties/{Type}/{ID}/{SubId}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + }, + '/parties/{Type}/{ID}/error': { + put: () => ctx.state.path.params.ID, + }, + '/parties/{Type}/{ID}/{SubId}/error': { + put: () => ctx.state.path.params.ID, + }, + '/quotes': { + post: () => ctx.request.body.quoteId, + }, + '/quotes/{ID}': { + put: () => ctx.state.path.params.ID, + }, + '/quotes/{ID}/error': { + put: () => ctx.state.path.params.ID, + }, + '/transfers': { + post: () => ctx.request.body.transferId, + }, + '/transfers/{ID}': { + get: () => ctx.state.path.params.ID, + put: () => ctx.state.path.params.ID, + patch: () => ctx.state.path.params.ID, + }, + '/transfers/{ID}/error': { + put: () => ctx.state.path.params.ID, + }, + '/transactionRequests': { + post: () => ctx.request.body.transactionRequestId, + }, + '/transactionRequests/{ID}': { + put: () => ctx.state.path.params.ID, + } + }[ctx.state.path.pattern]; + if (getters) { + const getter = getters[ctx.method.toLowerCase()]; + if (getter) { + ctx.state.fspiopId = getter(ctx.request); + } + } + await next(); +}; + + +/** + * cache incoming requests and callbacks + * @return {Function} + */ +const cacheRequest = (cache) => async (ctx, next) => { + if (ctx.state.fspiopId) { + const req = { + headers: ctx.request.headers, + data: ctx.request.body, + }; + const prefix = ctx.method.toLowerCase() === 'put' ? cache.CALLBACK_PREFIX : cache.REQUEST_PREFIX; + const res = await cache.set(`${prefix}${ctx.state.fspiopId}`, req); + ctx.state.logger.push({ res }).log('Caching request'); + } + await next(); +}; + + /** * tag each incoming request with a unique identifier * @return {Function} @@ -49,8 +163,13 @@ const createHeaderValidator = (logger) => async (ctx, next) => { 'application/vnd.interoperability.parties+json;version=1.0', 'application/vnd.interoperability.participants+json;version=1.0', 'application/vnd.interoperability.quotes+json;version=1.0', + 'application/vnd.interoperability.quotes+json;version=1.1', + 'application/vnd.interoperability.bulkQuotes+json;version=1.0', + 'application/vnd.interoperability.bulkQuotes+json;version=1.1', + 'application/vnd.interoperability.bulkTransfers+json;version=1.0', 'application/vnd.interoperability.transactionRequests+json;version=1.0', 'application/vnd.interoperability.transfers+json;version=1.0', + 'application/vnd.interoperability.transfers+json;version=1.1', 'application/vnd.interoperability.authorizations+json;version=1.0', 'application/json' ]); @@ -119,17 +238,25 @@ const createJwsValidator = (logger, keys, exclusions) => { }; +/** + * Add request state. + * TODO: this should probably be app context: + * https://github.com/koajs/koa/blob/master/docs/api/index.md#appcontext + * @param sharedState + * @return {Function} + */ +const applyState = (sharedState) => async (ctx, next) => { + Object.assign(ctx.state, sharedState); + await next(); +}; + + /** * Add a log context for each request, log the receipt and handling thereof * @param logger - * @param sharedState * @return {Function} */ -const createLogger = (logger, sharedState) => async (ctx, next) => { - ctx.state = { - ...ctx.state, - ...sharedState, - }; +const createLogger = (logger) => async (ctx, next) => { ctx.state.logger = logger.push({ request: { id: ctx.request.id, path: ctx.path, @@ -195,6 +322,9 @@ const createResponseBodyHandler = () => async (ctx, next) => { module.exports = { + applyState, + assignFspiopIdentifier, + cacheRequest, createErrorHandler, createRequestIdGenerator, createHeaderValidator, diff --git a/src/OAuthTestServer/index.js b/src/OAuthTestServer/index.js index 7773ca1e7..2c1ffa92d 100644 --- a/src/OAuthTestServer/index.js +++ b/src/OAuthTestServer/index.js @@ -10,10 +10,10 @@ 'use strict'; +const http = require('http'); const Koa = require('koa'); const koaBody = require('koa-body'); const OAuthServer = require('koa2-oauth-server'); -const { Logger, Transports } = require('@internal/log'); const { InMemoryCache } = require('./model'); class OAuthTestServer { @@ -23,56 +23,42 @@ class OAuthTestServer { * @param {number} conf.port OAuth server listen port * @param {string} conf.clientKey Customer Key * @param {String} conf.clientSecret Customer Secret - * @param {String} conf.logIndent + * @param {Logger} conf.logger Logger */ - constructor(conf) { - this._conf = conf; + constructor({ port, clientKey, clientSecret, logger }) { this._api = null; - this._logger = null; + this._port = port; + this._logger = logger; + this._api = OAuthTestServer._SetupApi({ clientKey, clientSecret }); + this._server = http.createServer(this._api.callback()); } async start() { - await new Promise((resolve) => this._api.listen(this._conf.port, resolve)); - this._logger.log(`Serving OAuth2 Test Server on port ${this._conf.port}`); + if (this._server.listening) { + return; + } + await new Promise((resolve) => this._server.listen(this._port, resolve)); + this._logger.push({ port: this._port }).log('Serving OAuth2 Test Server'); } async stop() { - if (this._api) { - return; - } - await new Promise(resolve => this._api.close(resolve)); - console.log('OAuth2 Test Server shut down complete'); + await new Promise(resolve => this._server.close(resolve)); + this._logger.log('OAuth2 Test Server shut down complete'); } - async setupApi() { - this._api = new Koa(); - this._logger = await this._createLogger(); + static _SetupApi({ clientKey, clientSecret }) { + const result = new Koa(); - this._api.oauth = new OAuthServer({ - model: new InMemoryCache(this._conf), + result.oauth = new OAuthServer({ + model: new InMemoryCache({ clientKey, clientSecret }), accessTokenLifetime: 60 * 60, allowBearerTokensInQueryString: true, }); - this._api.use(koaBody()); - this._api.use(this._api.oauth.token()); + result.use(koaBody()); + result.use(result.oauth.token()); - this._api.use(async (next) => { - this.body = 'Secret area'; - await next(); - }); - } - - async _createLogger() { - const transports = await Promise.all([Transports.consoleDir()]); - // Set up a logger for each running server - return new Logger({ - context: { - app: 'mojaloop-sdk-oauth-test-server' - }, - space: this._conf.logIndent, - transports, - }); + return result; } } diff --git a/src/OAuthTestServer/model.js b/src/OAuthTestServer/model.js index 2aef819c1..39588df3c 100644 --- a/src/OAuthTestServer/model.js +++ b/src/OAuthTestServer/model.js @@ -10,15 +10,13 @@ class InMemoryCache { /** - * - * @param {Object} opts * @param {string} opts.clientKey Customer Key * @param {String} opts.clientSecret Customer Secret */ - constructor(opts) { + constructor({ clientKey : clientId, clientSecret }) { this.clients = [{ - clientId : opts.clientKey, - clientSecret : opts.clientSecret, + clientId, + clientSecret, grants: [ 'client_credentials' ], diff --git a/src/OutboundServer/api.yaml b/src/OutboundServer/api.yaml index ee7bd1c6f..7afa9ddf3 100644 --- a/src/OutboundServer/api.yaml +++ b/src/OutboundServer/api.yaml @@ -125,6 +125,102 @@ paths: schema: $ref: '#/components/schemas/errorResponse' + /bulkTransfers: + post: + summary: Sends money from one account to multiple accounts + description: > + The HTTP request `POST /bulkTransfers` is used to request the movement of funds from payer DFSP to payees' DFSP. + tags: + - BulkTransfers + requestBody: + description: Bulk transfer request body + content: + application/json: + schema: + $ref: '#/components/schemas/bulkTransferRequest' + required: true + responses: + 200: + $ref: '#/components/responses/bulkTransferSuccess' + 400: + $ref: '#/components/responses/bulkTransferBadRequest' + 500: + $ref: '#/components/responses/bulkTransferServerError' + 504: + $ref: '#/components/responses/bulkTransferTimeout' + + /bulkTransfers/{bulkTransferId}: + get: + summary: Retrieves information for a specific bulk transfer + description: >- + The HTTP request `GET /bulkTransfers/{bulktTransferId}` is used to get information regarding a bulk transfer created or requested earlier. + The `{bulkTransferId}` in the URI should contain the `bulkTransferId` that was used for the creation of the bulk transfer. + tags: + - BulkTransfers + parameters: + - $ref: '#/components/parameters/bulkTransferId' + responses: + 200: + description: Bulk transfer information successfully retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/bulkTransferStatusResponse' + 500: + description: An error occurred processing the bulk transfer + content: + application/json: + schema: + $ref: '#/components/schemas/errorResponse' + + /bulkQuotes: + post: + summary: Request bulk quotes for the provided financial transactions + description: > + The HTTP request `POST /bulkQuotes` is used to request a bulk quote to fascilitate funds transfer from payer DFSP to payees' DFSP. + tags: + - BulkQuotes + requestBody: + description: Bulk quote request body + content: + application/json: + schema: + $ref: '#/components/schemas/bulkQuoteRequest' + required: true + responses: + 200: + $ref: '#/components/responses/bulkQuoteSuccess' + 400: + $ref: '#/components/responses/bulkQuoteBadRequest' + 500: + $ref: '#/components/responses/bulkQuoteServerError' + 504: + $ref: '#/components/responses/bulkQuoteTimeout' + + /bulkQuotes/{bulkQuoteId}: + get: + summary: Retrieves information for a specific bulk quote + description: >- + The HTTP request `GET /bulkQuotes/{bulktQuoteId}` is used to get information regarding a bulk quote created or requested earlier. + The `{bulkQuoteId}` in the URI should contain the `bulkQuoteId` that was used for the creation of the bulk quote. + tags: + - BulkQuotes + parameters: + - $ref: '#/components/parameters/bulkQuoteId' + responses: + 200: + description: Bulk quote information successfully retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/bulkQuoteStatusResponse' + 500: + description: An error occurred processing the bulk quote + content: + application/json: + schema: + $ref: '#/components/schemas/errorResponse' + /requestToPay: post: summary: Receiver requesting funds from Sender @@ -134,8 +230,8 @@ paths: The underlying API has two stages: 1. Party lookup. This facilitates a check by the sending party that the destination party is correct before proceeding with a money movement. - 2. Transaction Request. This request enables a Payee to request Payer to send electronic funds to the Payee. - + 2. Transaction Request. This request enables a Payee to request Payer to send electronic funds to the Payee. + tags: - RequestToPay requestBody: @@ -233,7 +329,7 @@ paths: $ref: '#/components/responses/transferServerError' 504: $ref: '#/components/responses/transferTimeout' - + /accounts: post: summary: Create accounts on the Account Lookup Service @@ -260,8 +356,130 @@ paths: 504: $ref: '#/components/responses/accountsCreationTimeout' + /authorizations: + post: + tags: + - authorizations + - sampled + description: The HTTP request `POST /authorizations` is used to request the Payer to enter the applicable credentials in the PISP system. + summary: /authorizations + operationId: AuthorizationsPost + requestBody: + description: Perform authorization + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/authorizationsRequest' + responses: + 200: + $ref: '#/components/responses/authorizationsResponse' + # TODO: define when ready to + # 400: + # $ref: '#/components/responses/authorizationsPostBadRequest' + # 500: + # $ref: '#/components/responses/authorizationsPostServerError' + # 504: + # $ref: '#/components/responses/authorizationsPostTimeout' + + /thirdpartyRequests/transactions: + post: + summary: Initiates a third party request transaction. + description: > + The HTTP request `POST /thirdpartyRequests/transactions` is sent to the Switch to initiate a third party request transaction. + tags: + - thirdpartyRequest + requestBody: + description: Thirdparty requests transaction request body. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/thirdpartyRequestsTransactionRequest' + responses: + 200: + $ref: '#/components/responses/thirdpartyRequestsTransactionResponse' + # 400: + # $ref: '#/components/responses/authorizationsPostBadRequest' + # 500: + # $ref: '#/components/responses/' + # 504: + # $ref: '#/components/responses/' + + /thirdpartyRequests/transactions/{transactionRequestId}: + get: + summary: Retrieves information for a specific thirdparty request transaction. + description: > + The HTTP request `GET /thirdpartyRequests/transactions/{transactionRequestId}` is used to get information regarding a thirdparty transaction created or requested earlier. + The `{transactionRequestId}` in the URI should contain the `ID` that was used for the creation of the thirdparty request transaction. + tags: + - thirdpartyRequest + parameters: + - $ref: '#/components/parameters/transactionRequestId' + responses: + 200: + description: Thirdparty requests transaction information successfully retrieved + content: + application/json: + schema: + $ref: '#/components/responses/thirdpartyRequestsTransactionResponse' + # 500: + # description: + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/' + + /parties/{Type}/{ID}: + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + get: + description: >- + The HTTP request GET /parties// (or GET /parties///) is used to lookup + information regarding the requested Party, defined by , and optionally + (for example, GET /parties/MSISDN/123456789, or GET + /parties/BUSINESS/shoecompany/employee1). + summary: PartiesByTypeAndID + tags: + - parties + operationId: PartiesByTypeAndID + responses: + '200': + $ref: '#/components/responses/PartiesByIdResponse' + '404': + $ref: '#/components/responses/PartiesByIdError404' + + /parties/{Type}/{ID}/{SubId}: + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/SubId' + get: + description: >- + The HTTP request GET /parties// (or GET /parties///) is used to lookup + information regarding the requested Party, defined by , and optionally + (for example, GET /parties/MSISDN/123456789, or GET + /parties/BUSINESS/shoecompany/employee1). + summary: PartiesSubIdByTypeAndID + tags: + - parties + operationId: PartiesSubIdByTypeAndID + responses: + '200': + $ref: '#/components/responses/PartiesByIdResponse' + '404': + $ref: '#/components/responses/PartiesByIdError404' + components: schemas: + accountAddress: + title: accountAddress + type: string + description: Unique routable address which is DFSP specific. + pattern: ^([0-9A-Za-z_~\-\.]+[0-9A-Za-z_~\-])$ + minLength: 1 + maxLength: 1023 amountType: type: string @@ -433,6 +651,8 @@ components: - XDR - XOF - XPF + - XTS + - XXX - YER - ZAR - ZMW @@ -463,6 +683,16 @@ components: transferState: $ref: '#/components/schemas/transferResponse' + bulkTransferErrorResponse: + allOf: + - $ref: '#/components/schemas/errorResponse' + - type: object + required: + - bulkTansferState + properties: + bulkTransferState: + $ref: '#/components/schemas/bulkTransferResponse' + errorAccountsResponse: allOf: - $ref: '#/components/schemas/errorResponse' @@ -484,6 +714,8 @@ components: - DEVICE - IBAN - ALIAS + - CONSENT + - THIRD_PARTY_LINK description: Below are the allowed values for the enumeration. - MSISDN - An MSISDN (Mobile Station International Subscriber Directory Number, that is, the phone number) is used as reference to a participant. The MSISDN identifier should be in international format according to the [ITU-T E.164 standard](https://www.itu.int/rec/T-REC-E.164/en). Optionally, the MSISDN may be prefixed by a single plus sign, indicating the international prefix. @@ -572,7 +804,6 @@ components: - OTHER_ID - Any other type of identification type number is used in reference to a party. - money: pattern: ^([0]|([1-9][0-9]{0,17}))([.][0-9]{0,3}[1-9])?$ type: string @@ -616,7 +847,7 @@ components: enum: - true - false - + transferContinuationAcceptOTP: type: object required: @@ -670,6 +901,8 @@ components: extensionList: $ref: '#/components/schemas/extensionList' + accounts: + $ref: '#/components/schemas/accountList' extensionItem: type: object properties: @@ -703,6 +936,27 @@ components: required: - extension + accountList: + type: array + items: + $ref: '#/components/schemas/account' + minItems: 1 + maxItems: 32 + + account: + type: object + properties: + address: + $ref: '#/components/schemas/accountAddress' + currency: + type: string + minLength: 3 + maxLength: 3 + description: + type: string + minLength: 1 + maxLength: 128 + transferRequest: type: object required: @@ -802,9 +1056,9 @@ components: $ref: '#/components/schemas/initiatorType' authenticationType: type: string - description: OTP or QR Code, otherwise empty + description: OTP or QR Code, otherwise empty requestToPayState: - $ref: '#/components/schemas/mojaloopTransactionRequestState' + $ref: '#/components/schemas/mojaloopTransactionRequestState' requestToPayTransferRequest: type: object @@ -943,6 +1197,23 @@ components: or an object representing other types of error e.g. exceptions that may occur inside the scheme adapter. $ref: '#/components/schemas/transferError' + bulkTransferResponse: + type: object + required: + - from + - individualTransferResults + properties: + transferId: + $ref: '#/components/schemas/mojaloopIdentifier' + from: + $ref: '#/components/schemas/transferParty' + individualTransferResults: + type: array + maxItems: 1000 + items: + $ref: '#/components/schemas/individualTransferResult' + description: List of individual transfer result in a bulk transfer response. + transferStatusResponse: type: object required: @@ -957,6 +1228,42 @@ components: fulfil: $ref: '#/components/schemas/transferFulfilment' + bulkTransferStatusResponse: + type: object + required: + - bulkTransferId + - currentState + - fulfils + properties: + bulkTransferId: + $ref: '#/components/schemas/mojaloopIdentifier' + currentState: + $ref: '#/components/schemas/bulkTransferStatus' + fulfils: + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: '#/components/schemas/individualTransferFulfilment' + + bulkQuoteStatusResponse: + type: object + required: + - bulkQuoteId + - currentState + - individualQuotes + properties: + bulkQuoteId: + $ref: '#/components/schemas/mojaloopIdentifier' + currentState: + $ref: '#/components/schemas/bulkQuoteStatus' + individualQuotes: + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: '#/components/schemas/individualQuote' + quote: type: object description: Mojaloop API response to a quote request @@ -1022,6 +1329,85 @@ components: required: - transferState + mojaloopAuthenticationType: + title: AuthenticationType + type: string + enum: + - OTP + - QRCODE + - U2F + description: Below are the allowed values for the enumeration AuthenticationType. + + mojaloopAuthenticationValue: + title: AuthenticationValue + oneOf: + - $ref: '#/components/schemas/mojaloopOtpValue' + - $ref: '#/components/schemas/mojaloopQRCODE' + - $ref: '#/components/schemas/mojaloopU2FPinValue' + description: Contains the authentication value. The format depends on the authentication type used in the AuthenticationInfo complex type. + + mojaloopOtpValue: + title: OtpValue + type: string + pattern: '^\d{3,10}$' + + mojaloopU2FPinValue: + title: U2FPinValue + type: object + description: U2F challenge-response, where payer FSP verifies if the response provided by end-user device matches the previously registered key. + properties: + pinValue: + $ref: '#/components/schemas/mojaloopU2FPIN' + description: U2F challenge-response. + counter: + $ref: '#/components/schemas/mojaloopInteger' + description: Sequential counter used for cloning detection. Present only for U2F authentication. + required: + - pinValue + - counter + + mojaloopU2FPIN: + title: U2FPIN + type: string + pattern: ^\S{1,64}$ + minLength: 1 + maxLength: 64 + description: U2F challenge-response, where payer FSP verifies if the response provided by end-user device matches the previously registered key. + + mojaloopQRCODE: + title: QRCODE + type: string + pattern: ^\S{1,64}$ + minLength: 1 + maxLength: 64 + description: QR code used as One Time Password. + + mojaloopAuthenticationInfo: + title: AuthenticationInfo + description: Data model for the complex type AuthenticationInfo + required: + - authentication + - authenticationValue + type: object + properties: + authentication: + $ref: '#/components/schemas/mojaloopAuthenticationType' + authenticationValue: + $ref: '#/components/schemas/mojaloopAuthenticationValue' + + individualTransferFulfilment: + type: object + description: A Mojaloop API transfer fulfilment for individual transfers in a bulk transfer + properties: + fulfilment: + $ref: '#/components/schemas/ilpFulfilment' + description: > + Fulfilment of the condition specified with the transaction. + Mandatory if transfer has completed successfully. + extensionList: + $ref: '#/components/schemas/extensionListComplex' + description: 'Optional extension, specific to deployment.' + mojaloopTransferState: type: string enum: @@ -1050,6 +1436,27 @@ components: sent the transaction request to the Payer. - ACCEPTED Payer has approved the transaction. - REJECTED Payer has rejected the transaction. + mojaloopCorrelationId: + title: CorrelationId + description: >- + Identifier that correlates all messages of the same sequence. The API + data type UUID (Universally Unique Identifier) is a JSON String in + canonical format, conforming to RFC 4122, that is restricted by a + regular expression for interoperability reasons. An UUID is always 36 + characters long, 32 hexadecimal symbols and 4 dashes (‘-‘). + pattern: >- + ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ + type: string + + mojaloopInteger: + title: Integer + description: >- + The API data type Integer is a JSON String consisting of digits only. + Negative numbers and leading zeroes are not allowed. The data type is + always limited to a specific number of digits. + pattern: '^[1-9]\d*$' + type: string + mojaloopMoney: title: Money type: object @@ -1065,38 +1472,203 @@ components: - currency - amount - transferError: + mojaloopQuotesIDPutResponse: + title: QuotesIDPutResponse + description: 'PUT /quotes/{ID} object' + required: + - transferAmount + - expiration + - ilpPacket + - condition type: object - description: This object represents a Mojaloop API error received at any time during the transfer process + additionalProperties: false properties: - httpStatusCode: - type: integer - description: The HTTP status code returned to the caller. This is the same as the actual HTTP status code returned with the response. - mojaloopError: - description: If a transfer process results in an error callback during the asynchronous Mojaloop API exchange, this property will contain the underlying Mojaloop API error object. - $ref: '#/components/schemas/mojaloopError' + transferAmount: + $ref: '#/components/schemas/mojaloopMoney' + payeeReceiveAmount: + $ref: '#/components/schemas/mojaloopMoney' + payeeFspFee: + $ref: '#/components/schemas/mojaloopMoney' + payeeFspCommission: + $ref: '#/components/schemas/mojaloopMoney' + expiration: + description: >- + Date and time until when the quotation is valid and can be honored + when used in the subsequent transaction. + type: string + geoCode: + $ref: '#/components/schemas/geoCode' + ilpPacket: + description: The ILP Packet that must be attached to the transfer by the Payer. + type: string + condition: + description: The condition that must be attached to the transfer by the Payer. + type: string + extensionList: + $ref: '#/components/schemas/extensionList' - generalError: + mojaloopParty: + title: mojaloopParty type: object - description: This object may represent a number of different error object types and so its properties may vary significantly. + description: Data model for the complex type Party. + properties: + accounts: + $ref: '#/components/schemas/accountList' + description: List of accounts associated with the party containing and DFSP routable address, currency identifier and description. + partyIdInfo: + $ref: '#/components/schemas/mojaloopPartyIdInfo' + description: Party Id type, id, sub ID or type, and FSP Id. + merchantClassificationCode: + type: string + description: Used in the context of Payee Information, where the Payee happens to be a merchant accepting merchant payments. + example: 4321 + name: + type: string + description: Display name of the Party, could be a real name or a nick name. + example: Henrik Karlsson + personalInfo: + $ref: '#/components/schemas/mojaloopPartyPersonalInfo' + description: Personal information used to verify identity of Party such as first, middle, last name and date of birth. + required: + - partyIdInfo - mojaloopError: + mojaloopPartyIdInfo: + title: mojaloopPartyIdInfo type: object + description: Data model for the complex type PartyIdInfo. properties: - errorInformation: - $ref: '#/components/schemas/errorInformation' + partyIdType: + type: string + description: Type of the identifier. + example: PERSONAL_ID + partyIdentifier: + type: string + description: An identifier for the Party. + example: 16135551212 + partySubIdOrType: + type: string + description: A sub-identifier or sub-type for the Party. + example: DRIVING_LICENSE + fspId: + type: string + description: FSP ID (if known). + example: 1234 + required: + - partyIdType + - partyIdentifier - errorInformation: - title: ErrorInformation + mojaloopPartyPersonalInfo: + title: mojaloopPartyPersonalInfo type: object - description: A Mojaloop API error information construct. + description: Data model for the complex type mojaloopPartyPersonalInfo. properties: - errorCode: - $ref: '#/components/schemas/errorCode' - description: Specific error number. - errorDescription: - $ref: '#/components/schemas/errorDescription' - description: Error description string. + complexName: + $ref: '#/components/schemas/mojaloopPartyComplexName' + description: First, middle and last name for the Party. + dateOfBirth: + type: string + description: Date of birth for the Party. + example: '1966-06-16' + + mojaloopPartyComplexName: + title: PartyComplexName + type: object + description: Data model for the complex type mojaloopPartyComplexName. + properties: + firstName: + type: string + description: Party’s first name. + example: Henrik + middleName: + type: string + description: Party’s middle name. + example: Johannes + lastName: + type: string + description: Party’s last name. + example: Karlsson + + mojaloopTransactionType: + title: TransactionType + type: object + description: Data model for the complex type mojaloopTransactionType. + properties: + scenario: + type: string + description: Deposit, withdrawal, refund, … + example: DEPOSIT + subScenario: + type: string + description: Possible sub-scenario, defined locally within the scheme. + example: Locally defined sub-scenario. + initiator: + type: string + description: Who is initiating the transaction - Payer or Payee. + example: PAYEE + initiatorType: + type: string + description: Consumer, agent, business, … + example: CONSUMER + refundInfo: + $ref: '#/components/schemas/mojaloopRefund' + description: Extra information specific to a refund scenario. Should only be populated if scenario is REFUND. + balanceOfPayments: + type: string + description: Balance of Payments code. + example: 123 + required: + - scenario + - initiator + - initiatorType + + mojaloopRefund: + title: mojaloopRefund + type: object + description: Data model for the complex type mojaloopRefund. + properties: + originalTransactionId: + type: string + description: Reference to the original transaction ID that is requested to be refunded. + example: b51ec534-ee48-4575-b6a9-ead2955b8069 + refundReason: + type: string + description: Free text indicating the reason for the refund. + example: Free text indicating reason for the refund. + required: + - originalTransactionId + + transferError: + type: object + description: This object represents a Mojaloop API error received at any time during the transfer process + properties: + httpStatusCode: + type: integer + description: The HTTP status code returned to the caller. This is the same as the actual HTTP status code returned with the response. + mojaloopError: + description: If a transfer process results in an error callback during the asynchronous Mojaloop API exchange, this property will contain the underlying Mojaloop API error object. + $ref: '#/components/schemas/mojaloopError' + + generalError: + type: object + description: This object may represent a number of different error object types and so its properties may vary significantly. + + mojaloopError: + type: object + properties: + errorInformation: + $ref: '#/components/schemas/errorInformation' + + errorInformation: + title: ErrorInformation + type: object + description: A Mojaloop API error information construct. + properties: + errorCode: + $ref: '#/components/schemas/errorCode' + description: Specific error number. + errorDescription: + $ref: '#/components/schemas/errorDescription' + description: Error description string. extensionList: $ref: '#/components/schemas/extensionListComplex' description: 'Optional list of extensions, specific to deployment.' @@ -1132,6 +1704,18 @@ components: - WAITING_FOR_QUOTE_ACCEPTANCE - COMPLETED + bulkTransferStatus: + type: string + enum: + - ERROR_OCCURRED + - COMPLETED + + bulkQuoteStatus: + type: string + enum: + - ERROR_OCCURRED + - COMPLETED + geoCode: type: object description: > @@ -1238,6 +1822,847 @@ components: lastError: $ref: '#/components/schemas/transferError' + authorizationsRequest: + title: authorizationsRequest + description: POST /authorizations Request object + type: object + properties: + toParticipantId: + type: string + authenticationType: + $ref: '#/components/schemas/mojaloopAuthenticationType' + retriesLeft: + $ref: '#/components/schemas/mojaloopInteger' + amount: + $ref: '#/components/schemas/mojaloopMoney' + transactionId: + $ref: '#/components/schemas/mojaloopCorrelationId' + transactionRequestId: + $ref: '#/components/schemas/mojaloopCorrelationId' + quote: + $ref: '#/components/schemas/mojaloopQuotesIDPutResponse' + required: + - authenticationType + - retriesLeft + - amount + - transactionId + - transactionRequestId + - quote + additionalProperties: false + + authorizationsResponse: + title: authorizationsResponse + description: | + response body of POST/authorizations + derived from AuthorizationsIDPutResponse + type: object + required: + - responseType + additionalProperties: false + properties: + authenticationInfo: + $ref: '#/components/schemas/mojaloopAuthenticationInfo' + responseType: + description: >- + Enum containing response information; if the customer entered the + authentication value, rejected the transaction, or requested a + resend of the authentication value. + TODO: enum specification will be defined when all values will be known + type: string + currentState: + $ref: '#/components/schemas/authorizationsState' + authorizationsState: + title: authorizationsState + description: state of POST authorizations + type: string + enum: + - WAITING_FOR_AUTHORIZATION_REQUEST + - COMPLETED + - ERROR_OCCURRED + + bulkTransferRequest: + type: object + required: + - homeTransactionId + - from + - individualTransfers + properties: + homeTransactionId: + type: string + description: Transaction ID from the DFSP backend, used to reconcile transactions between the Switch and DFSP backend systems. + bulkTransferId: + $ref: '#/components/schemas/mojaloopIdentifier' + from: + $ref: '#/components/schemas/transferParty' + individualTransfers: + description: List of individual transfers in a bulk transfer. + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: '#/components/schemas/individualTransfer' + extensions: + $ref: '#/components/schemas/extensionList' + + individualTransfer: + title: IndividualTransfer + type: object + description: Data model for the complex type 'individualTransfer'. + properties: + transferId: + $ref: '#/components/schemas/mojaloopIdentifier' + to: + $ref: '#/components/schemas/transferParty' + amountType: + $ref: '#/components/schemas/amountType' + currency: + $ref: '#/components/schemas/currency' + amount: + $ref: '#/components/schemas/money' + transactionType: + $ref: '#/components/schemas/transactionType' + note: + maxLength: 128 + type: string + extensions: + $ref: '#/components/schemas/extensionList' + required: + - transferId + - to + - amountType + - currency + - transactionType + + individualTransferResult: + type: object + properties: + transferId: + $ref: '#/components/schemas/mojaloopIdentifier' + to: + $ref: '#/components/schemas/transferParty' + amountType: + $ref: '#/components/schemas/amountType' + currency: + $ref: '#/components/schemas/currency' + amount: + $ref: '#/components/schemas/money' + transactionType: + $ref: '#/components/schemas/transactionType' + note: + maxLength: 128 + type: string + quoteId: + $ref: '#/components/schemas/mojaloopIdentifier' + quoteResponse: + $ref: '#/components/schemas/quote' + quoteResponseSource: + type: string + description: > + FSPID of the entity that supplied the quote response. This may not be the same as the FSPID of the entity which + owns the end user account in the case of a FOREX transfer. i.e. it may be a FOREX gateway. + fulfil: + $ref: '#/components/schemas/transferFulfilment' + lastError: + description: > + Object representing the last error to occur during a transfer process. This may be a Mojaloop API error returned from another entity in the scheme + or an object representing other types of error e.g. exceptions that may occur inside the scheme adapter. + $ref: '#/components/schemas/transferError' + + bulkQuoteRequest: + type: object + required: + - homeTransactionId + - from + - individualQuotes + properties: + homeTransactionId: + type: string + description: Transaction ID from the DFSP backend, used to reconcile transactions between the Switch and DFSP backend systems. + bulkQuoteId: + $ref: '#/components/schemas/mojaloopIdentifier' + from: + $ref: '#/components/schemas/transferParty' + individualQuotes: + description: List of individual quotes in a bulk quote. + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: '#/components/schemas/individualQuote' + extensions: + $ref: '#/components/schemas/extensionList' + + individualQuote: + title: IndividualQuote + type: object + description: Data model for the complex type 'individualQuote'. + properties: + quoteId: + $ref: '#/components/schemas/mojaloopIdentifier' + to: + $ref: '#/components/schemas/transferParty' + amountType: + $ref: '#/components/schemas/amountType' + currency: + $ref: '#/components/schemas/currency' + amount: + $ref: '#/components/schemas/money' + transactionType: + $ref: '#/components/schemas/transactionType' + note: + maxLength: 128 + type: string + extensions: + $ref: '#/components/schemas/extensionList' + required: + - quoteId + - to + - amountType + - currency + - transactionType + + bulkQuoteResponse: + type: object + required: + - from + - individualQuoteResults + properties: + quoteId: + $ref: '#/components/schemas/mojaloopIdentifier' + homeTransactionId: + type: string + description: Transaction ID from the DFSP backend, used to reconcile transactions between the Switch and DFSP backend systems. + from: + $ref: '#/components/schemas/transferParty' + individualQuoteResults: + type: array + maxItems: 1000 + items: + $ref: '#/components/schemas/individualQuoteResult' + description: List of individualQuoteResults in a bulk transfer response. + + individualQuoteResult: + type: object + properties: + quoteId: + $ref: '#/components/schemas/mojaloopIdentifier' + to: + $ref: '#/components/schemas/transferParty' + amountType: + $ref: '#/components/schemas/amountType' + currency: + $ref: '#/components/schemas/currency' + amount: + $ref: '#/components/schemas/money' + transactionType: + $ref: '#/components/schemas/transactionType' + note: + maxLength: 128 + type: string + lastError: + description: > + Object representing the last error to occur during a quote process. This may be a Mojaloop API error returned from another entity in the scheme + or an object representing other types of error e.g. exceptions that may occur inside the scheme adapter. + $ref: '#/components/schemas/quoteError' + + quoteError: + type: object + description: This object represents a Mojaloop API error received at any time during the quote process + properties: + httpStatusCode: + type: integer + description: The HTTP status code returned to the caller. This is the same as the actual HTTP status code returned with the response. + mojaloopError: + description: If a quote process results in an error callback during the asynchronous Mojaloop API exchange, this property will contain the underlying Mojaloop API error object. + $ref: '#/components/schemas/mojaloopError' + + bulkQuoteErrorResponse: + allOf: + - $ref: '#/components/schemas/errorResponse' + - type: object + required: + - bulkTansferState + properties: + bulkQuoteState: + $ref: '#/components/schemas/bulkQuoteResponse' + thirdpartyRequestsTransactionRequest: + title: thirdpartyRequestTransaction + type: object + description: Object. + properties: + transactionRequestId: + $ref: '#/components/schemas/mojaloopCorrelationId' + description: > + Common ID between the FSPs for the transaction request object. + The ID should be reused for resends of the same transaction request. + A new ID should be generated for each new transaction request. + sourceAccountId: + $ref: '#/components/schemas/accountAddress' + description: DFSP specific account identifiers, e.g. `dfspa.alice.1234` + consentId: + allOf: + - $ref: '#/components/schemas/mojaloopCorrelationId' + description: > + Common ID between the PISP and FSP for the Consent object + This tells DFSP and auth-service which constent allows the PISP to initiate transaction. + payee: + $ref: '#/components/schemas/mojaloopParty' + description: Information about the Payee in the proposed financial transaction. + payer: + $ref: '#/components/schemas/mojaloopParty' + description: Information about the Payer in the proposed financial transaction. + amountType: + $ref: '#/components/schemas/amountType' + description: SEND for sendAmount, RECEIVE for receiveAmount. + amount: + $ref: '#/components/schemas/mojaloopMoney' + description: Requested amount to be transferred from the Payer to Payee. + transactionType: + $ref: '#/components/schemas/mojaloopTransactionType' + description: Type of transaction. + expiration: + $ref: '#/components/schemas/timestamp' + description: > + Date and time until when the transaction request is valid. + It can be set to get a quick failure in case the peer FSP takes too long to respond. + example: '2016-05-24T08:38:08.699-04:00' + required: + - transactionRequestId + - sourceAccountId + - consentId + - payee + - payer + - amountType + - amount + - transactionType + - expiration + thirdpartyRequestsTransactionResponse: + title: thirdpartyTransactionResponse + type: object + description: The object sent in the PUT /thirdpartyRequests/transactions/{ID} request. + properties: + transactionId: + $ref: '#/components/schemas/mojaloopCorrelationId' + description: > + Identifies a related transaction (if a transaction has been created) + transactionRequestState: + $ref: '#/components/schemas/mojaloopTransactionRequestState' + description: State of the transaction request` + + Party: + title: Party + type: object + description: Data model for the complex type Party. + properties: + accounts: + $ref: '#/components/schemas/AccountList' + description: List of accounts associated with the party containing and DFSP routable address, currency identifier and description. + partyIdInfo: + $ref: '#/components/schemas/PartyIdInfo' + description: 'Party Id type, id, sub ID or type, and FSP Id.' + merchantClassificationCode: + $ref: '#/components/schemas/MerchantClassificationCode' + description: >- + Used in the context of Payee Information, where the Payee happens to + be a merchant accepting merchant payments. + name: + $ref: '#/components/schemas/PartyName' + description: 'Display name of the Party, could be a real name or a nick name.' + personalInfo: + $ref: '#/components/schemas/PartyPersonalInfo' + description: >- + Personal information used to verify identity of Party such as first, + middle, last name and date of birth. + required: + - partyIdInfo + + PartyName: + title: PartyName + type: string + minLength: 1 + maxLength: 128 + description: Name of the Party. Could be a real name or a nickname. + + PartyPersonalInfo: + title: PartyPersonalInfo + type: object + description: Data model for the complex type PartyPersonalInfo. + properties: + complexName: + $ref: '#/components/schemas/PartyComplexName' + description: 'First, middle and last name for the Party.' + dateOfBirth: + $ref: '#/components/schemas/DateOfBirth' + description: Date of birth for the Party. + + PartyComplexName: + title: PartyComplexName + type: object + description: Data model for the complex type PartyComplexName. + properties: + firstName: + $ref: '#/components/schemas/FirstName' + description: Party’s first name. + middleName: + $ref: '#/components/schemas/MiddleName' + description: Party’s middle name. + lastName: + $ref: '#/components/schemas/LastName' + description: Party’s last name. + + FirstName: + title: FirstName + type: string + minLength: 1 + maxLength: 128 + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: First name of the Party (Name Type). + + MiddleName: + title: MiddleName + type: string + minLength: 1 + maxLength: 128 + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: Middle name of the Party (Name Type). + + LastName: + title: LastName + type: string + minLength: 1 + maxLength: 128 + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: Last name of the Party (Name Type). + + DateOfBirth: + title: DateofBirth (type Date) + type: string + pattern: >- + ^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)$ + description: Date of Birth of the Party. + + PartyIdInfo: + title: PartyIdInfo + type: object + description: Data model for the complex type PartyIdInfo. + properties: + partyIdType: + $ref: '#/components/schemas/PartyIdType' + description: Type of the identifier. + partyIdentifier: + $ref: '#/components/schemas/PartyIdentifier' + description: An identifier for the Party. + partySubIdOrType: + $ref: '#/components/schemas/PartySubIdOrType' + description: A sub-identifier or sub-type for the Party. + fspId: + $ref: '#/components/schemas/FspId' + description: FSP ID (if known) + extensionList: + $ref: '#/components/schemas/ExtensionList' + description: 'Optional extension, specific to deployment.' + required: + - partyIdType + - partyIdentifier + + PartyIdType: + title: PartyIdTypeEnum + type: string + enum: + - MSISDN + - EMAIL + - PERSONAL_ID + - BUSINESS + - DEVICE + - ACCOUNT_ID + - IBAN + - ALIAS + - CONSENT + - THIRD_PARTY_LINK + description: >- + Below are the allowed values for the enumeration - MSISDN An MSISDN + (Mobile Station International Subscriber Directory Number, that is, the + phone number) is used as reference to a participant. The MSISDN + identifier should be in international format according to the ITU-T + E.164 standard. Optionally, the MSISDN may be prefixed by a single plus + sign, indicating the international prefix. - EMAIL An email is used as + reference to a participant. The format of the email should be according + to the informational RFC 3696. - PERSONAL_ID A personal identifier is + used as reference to a participant. Examples of personal identification + are passport number, birth certificate number, and national registration + number. The identifier number is added in the PartyIdentifier element. + The personal identifier type is added in the PartySubIdOrType element. - + BUSINESS A specific Business (for example, an organization or a company) + is used as reference to a participant. The BUSINESS identifier can be in + any format. To make a transaction connected to a specific username or + bill number in a Business, the PartySubIdOrType element should be used. + - DEVICE A specific device (for example, a POS or ATM) ID connected to a + specific business or organization is used as reference to a Party. For + referencing a specific device under a specific business or organization, + use the PartySubIdOrType element. - ACCOUNT_ID A bank account number or + FSP account ID should be used as reference to a participant. The + ACCOUNT_ID identifier can be in any format, as formats can greatly + differ depending on country and FSP. - IBAN A bank account number or FSP + account ID is used as reference to a participant. The IBAN identifier + can consist of up to 34 alphanumeric characters and should be entered + without whitespace. - ALIAS An alias is used as reference to a + participant. The alias should be created in the FSP as an alternative + reference to an account owner. Another example of an alias is a username + in the FSP system. The ALIAS identifier can be in any format. It is also + possible to use the PartySubIdOrType element for identifying an account + under an Alias defined by the PartyIdentifier. + + PartyIdentifier: + title: PartyIdentifier + type: string + minLength: 1 + maxLength: 128 + description: Identifier of the Party. + + PartySubIdOrType: + title: PartySubIdOrType + type: string + minLength: 1 + maxLength: 128 + description: >- + Either a sub-identifier of a PartyIdentifier, or a sub-type of the + PartyIdType, normally a PersonalIdentifierType. + Currency: + title: CurrencyEnum + description: >- + The currency codes defined in ISO 4217 as three-letter alphabetic codes + are used as the standard naming representation for currencies. + type: string + minLength: 3 + maxLength: 3 + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KMF + - KPW + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRO + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLL + - SOS + - SPL + - SRD + - STD + - SVC + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VEF + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWD + Name: + title: Name + type: string + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: >- + The API data type Name is a JSON String, restricted by a regular + expression to avoid characters which are generally not used in a name. + Regular Expression - The regular expression for restricting the Name + type is "^(?!\s*$)[\w .,'-]{1,128}$". The restriction does not allow a + string consisting of whitespace only, all Unicode characters are + allowed, as well as the period (.) (apostrophe (‘), dash (-), comma (,) + and space characters ( ). Note - In some programming languages, Unicode + support must be specifically enabled. For example, if Java is used the + flag UNICODE_CHARACTER_CLASS must be enabled to allow Unicode + characters. + + FspId: + title: FspId + type: string + minLength: 1 + maxLength: 32 + description: FSP identifier. + + AccountList: + title: AccountList + type: object + description: Data model for the complex type AccountList + properties: + account: + type: array + items: + $ref: '#/components/schemas/Account' + minItems: 1 + maxItems: 32 + description: Accounts associated with the Party + required: + - account + + Account: + title: Account + type: object + description: Data model for the complex type Account + properties: + address: + $ref: '#/components/schemas/AccountAddress' + type: string + description: Unique routable address which is DFSP specific. + currency: + $ref: '#/components/schemas/Currency' + type: string + description: Currency of the amount. + description: + $ref: '#/components/schemas/Name' + type: string + description: The name of the account. + required: + - address + - currency + - description + AccountAddress: + title: AccountAddress + type: string + description: Unique routable address which is DFSP specific. + pattern: ^([0-9A-Za-z_~\-\.]+[0-9A-Za-z_~\-])$ + minLength: 1 + maxLength: 1023 + + PartiesCurrentState: + type: string + enum: + - 'WAITING_FOR_REQUEST_PARTY_INFORMATION' + - 'COMPLETED' + - 'ERROR_OCCURRED' + + MerchantClassificationCode: + title: MerchantClassificationCode + type: string + pattern: '^[\d]{1,4}$' + description: >- + A limited set of pre-defined numbers. This list would be a limited set + of numbers identifying a set of popular merchant types like School Fees, + Pubs and Restaurants, Groceries, etc. + + ErrorInformation: + title: ErrorInformation + type: object + description: Data model for the complex type ErrorInformation. + properties: + errorCode: + $ref: '#/components/schemas/ErrorCode' + description: Specific error number. + errorDescription: + $ref: '#/components/schemas/ErrorDescription' + description: Error description string. + extensionList: + $ref: '#/components/schemas/ExtensionList' + description: 'Optional list of extensions, specific to deployment.' + required: + - errorCode + - errorDescription + + ErrorCode: + title: ErrorCode + type: string + pattern: '^[1-9]\d{3}$' + description: >- + The API data type ErrorCode is a JSON String of four characters, + consisting of digits only. Negative numbers are not allowed. A leading + zero is not allowed. Each error code in the API is a four-digit number, + for example, 1234, where the first number (1 in the example) represents + the high-level error category, the second number (2 in the example) + represents the low-level error category, and the last two numbers (34 in + the example) represents the specific error. + + ErrorDescription: + title: ErrorDescription + type: string + minLength: 1 + maxLength: 128 + description: Error description string. + + ExtensionKey: + title: ExtensionKey + type: string + minLength: 1 + maxLength: 32 + description: Extension key. + + ExtensionValue: + title: ExtensionValue + type: string + minLength: 1 + maxLength: 128 + description: Extension value. + + Extension: + title: Extension + type: object + description: Data model for the complex type Extension + properties: + key: + $ref: '#/components/schemas/ExtensionKey' + description: Extension key. + value: + $ref: '#/components/schemas/ExtensionValue' + description: Extension value. + required: + - key + - value + + ExtensionList: + title: ExtensionList + type: object + description: Data model for the complex type ExtensionList + properties: + extension: + type: array + items: + $ref: '#/components/schemas/Extension' + minItems: 1 + maxItems: 16 + description: Number of Extension elements + required: + - extension + responses: transferSuccess: description: Transfer completed successfully @@ -1264,6 +2689,56 @@ components: schema: $ref: '#/components/schemas/errorTransferResponse' + bulkTransferSuccess: + description: Bulk transfer completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/bulkTransferResponse' + bulkTransferBadRequest: + description: Malformed or missing required body, headers or parameters + content: + application/json: + schema: + $ref: '#/components/schemas/bulkTransferErrorResponse' + bulkTransferServerError: + description: An error occurred processing the bulk transfer + content: + application/json: + schema: + $ref: '#/components/schemas/bulkTransferErrorResponse' + bulkTransferTimeout: + description: Timeout occurred processing the bulk transfer + content: + application/json: + schema: + $ref: '#/components/schemas/bulkTransferErrorResponse' + + bulkQuoteSuccess: + description: Bulk quote completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/bulkQuoteResponse' + bulkQuoteBadRequest: + description: Malformed or missing required body, headers or parameters + content: + application/json: + schema: + $ref: '#/components/schemas/bulkQuoteErrorResponse' + bulkQuoteServerError: + description: An error occurred processing the bulk quote + content: + application/json: + schema: + $ref: '#/components/schemas/bulkQuoteErrorResponse' + bulkQuoteTimeout: + description: Timeout occurred processing the bulk quote + content: + application/json: + schema: + $ref: '#/components/schemas/bulkQuoteErrorResponse' + accountsCreationCompleted: description: Accounts creation completed content: @@ -1281,7 +2756,7 @@ components: content: application/json: schema: - $ref: '#/components/schemas/errorAccountsResponse' + $ref: '#/components/schemas/errorAccountsResponse' requestToPaySuccess: description: Request to Pay completed successfully content: @@ -1300,8 +2775,45 @@ components: application/json: schema: $ref: '#/components/schemas/errorTransferResponse' + authorizationsResponse: + description: authorization response + content: + application/json: + schema: + $ref: '#/components/schemas/authorizationsResponse' + thirdpartyRequestsTransactionResponse: + description: Thirdparty requests transaction response + content: + application/json: + schema: + $ref: '#/components/schemas/thirdpartyRequestsTransactionResponse' - + PartiesByIdResponse: + description: PartiesByIdResponse + content: + application/json: + schema: + type: object + description: 'GET /parties/{Type}/{ID} response object' + properties: + party: + $ref: '#/components/schemas/Party' + description: Information regarding the requested Party. + currentState: + $ref: '#/components/schemas/PartiesCurrentState' + required: + - party + - currentState + + PartiesByIdError404: + description: PartiesByIdError404 + content: + application/json: + schema: + type: object + properties: + errorInformation: + $ref: '#/components/schemas/ErrorInformation' parameters: transferId: @@ -1312,6 +2824,22 @@ components: pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ type: string description: Identifier of the transfer to continue as returned in the response to a `POST /transfers` request. + bulkTransferId: + name: bulkTransferId + in: path + required: true + schema: + pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ + type: string + description: Identifier of the bulk transfer to continue as returned in the response to a `POST /bulkTransfers` request. + bulkQuoteId: + name: bulkQuoteId + in: path + required: true + schema: + pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ + type: string + description: Identifier of the bulk transfer to continue as returned in the response to a `POST /bulkTransfers` request. requestToPayTransactionId: name: requestToPayTransactionId in: path @@ -1320,3 +2848,30 @@ components: pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ type: string description: Identifier of the merchant request to pay transfer to continue as returned in the response to a `POST /requestToPayTransfer` request. + transactionRequestId: + name: transactionRequestId + in: path + required: true + schema: + pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ + type: string + description: Identifier of the thirdparty transaction request. + ID: + name: ID + in: path + required: true + schema: + type: string + Type: + name: Type + in: path + required: true + schema: + $ref: '#/components/schemas/PartyIdType' + SubId: + name: SubId + in: path + required: true + schema: + type: string + diff --git a/src/OutboundServer/handlers.js b/src/OutboundServer/handlers.js index 576d8a260..613812c44 100644 --- a/src/OutboundServer/handlers.js +++ b/src/OutboundServer/handlers.js @@ -6,13 +6,25 @@ * * * ORIGINAL AUTHOR: * * James Bush - james.bush@modusbox.com * + * CONTRIBUTORS: * + * Steven Oderayi - steven.oderayi@modusbox.com * **************************************************************************/ 'use strict'; const util = require('util'); -const { AccountsModel, OutboundTransfersModel, OutboundRequestToPayTransferModel, OutboundRequestToPayModel } = require('@internal/model'); +const { + AccountsModel, + OutboundTransfersModel, + OutboundBulkTransfersModel, + OutboundRequestToPayTransferModel, + OutboundRequestToPayModel, + OutboundBulkQuotesModel, + OutboundAuthorizationsModel, + OutboundThirdpartyTransactionModel, + PartiesModel +} = require('@internal/model'); /** @@ -60,6 +72,12 @@ const handleError = (method, err, ctx, stateField) => { const handleTransferError = (method, err, ctx) => handleError(method, err, ctx, 'transferState'); +const handleBulkTransferError = (method, err, ctx) => + handleError(method, err, ctx, 'bulkTransferState'); + +const handleBulkQuoteError = (method, err, ctx) => + handleError(method, err, ctx, 'bulkQuoteState'); + const handleAccountsError = (method, err, ctx) => handleError(method, err, ctx, 'executionState'); @@ -69,7 +87,16 @@ const handleRequestToPayError = (method, err, ctx) => const handleRequestToPayTransferError = (method, err, ctx) => handleError(method, err, ctx, 'requestToPayTransferState'); +const handleAuthorizationsError = (method, err, ctx) => + handleError(method, err, ctx, 'authorizationsState'); + +const handleThirdpartyRequestsTransactionsError = (method, err, ctx) => + handleError(method, err, ctx, 'thirdpartyRequestsTransactionsState'); + +const handleRequestPartiesInformationError = (method, err, ctx) => + handleError(method, err, ctx, 'requestPartiesInformationState'); + /** * Handler for outbound transfer request initiation */ @@ -85,7 +112,7 @@ const postTransfers = async (ctx) => { ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, }); // initialize the transfer model and start it running @@ -101,36 +128,6 @@ const postTransfers = async (ctx) => { } }; -/** - * Handler for outbound transfer request initiation - */ -const postRequestToPayTransfer = async (ctx) => { - try { - // this requires a multi-stage sequence with the switch. - let requestToPayTransferRequest = { - ...ctx.request.body - }; - - // use the merchant transfers model to execute asynchronous stages with the switch - const model = new OutboundRequestToPayTransferModel({ - ...ctx.state.conf, - cache: ctx.state.cache, - logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, - }); - - // initialize the transfer model and start it running - await model.initialize(requestToPayTransferRequest); - const response = await model.run(); - // return the result - ctx.response.status = 200; - ctx.response.body = response; - } - catch(err) { - return handleRequestToPayTransferError('postRequestToPayTransfer', err, ctx); - } -}; - /** * Handler for outbound transfer request */ @@ -147,7 +144,7 @@ const getTransfers = async (ctx) => { ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, }); // initialize the transfer model and start it running @@ -163,8 +160,6 @@ const getTransfers = async (ctx) => { } }; - - /** * Handler for resuming outbound transfers in scenarios where two-step transfers are enabled * by disabling the autoAcceptQuote SDK option @@ -177,7 +172,7 @@ const putTransfers = async (ctx) => { ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, }); // TODO: check the incoming body to reject party or quote when requested to do so @@ -196,6 +191,157 @@ const putTransfers = async (ctx) => { } }; +/** + * Handler for outbound bulk transfer request + */ +const postBulkTransfers = async (ctx) => { + try { + // this requires a multi-stage sequence with the switch. + let bulkTransferRequest = { + ...ctx.request.body + }; + + // use the bulk transfers model to execute asynchronous stages with the switch + const model = new OutboundBulkTransfersModel({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }); + + await model.initialize(bulkTransferRequest); + const response = await model.run(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + } + catch (err) { + return handleBulkTransferError('postBulkTransfers', err, ctx); + } +}; + +/** + * Handler for outbound bulk transfer request + */ +const getBulkTransfers = async (ctx) => { + try { + const bulkTransferRequest = { + ...ctx.request.body, + bulkTransferId: ctx.state.path.params.bulkTransferId, + currentState: 'getBulkTransfer', + }; + + // use the bulk transfers model to execute asynchronous stages with the switch + const model = new OutboundBulkTransfersModel({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }); + + await model.initialize(bulkTransferRequest); + const response = await model.getBulkTransfer(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + } + catch (err) { + return handleBulkTransferError('getBulkTransfers', err, ctx); + } +}; + +/** + * Handler for outbound bulk quote request + */ +const postBulkQuotes = async (ctx) => { + try { + let bulkQuoteRequest = { + ...ctx.request.body + }; + + // use the bulk quotes model to execute asynchronous request with the switch + const model = new OutboundBulkQuotesModel({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }); + + await model.initialize(bulkQuoteRequest); + const response = await model.run(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + } + catch (err) { + return handleBulkQuoteError('postBulkQuotes', err, ctx); + } +}; + +/** + * Handler for outbound bulk quote request + */ +const getBulkQuoteById = async (ctx) => { + try { + const bulkQuoteRequest = { + ...ctx.request.body, + bulkQuoteId: ctx.state.path.params.bulkQuoteId, + currentState: 'getBulkQuote', + }; + + // use the bulk quotes model to execute asynchronous stages with the switch + const model = new OutboundBulkQuotesModel({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }); + + await model.initialize(bulkQuoteRequest); + const response = await model.getBulkQuote(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + } + catch (err) { + return handleBulkQuoteError('getBulkQuoteById', err, ctx); + } +}; + +/** + * Handler for outbound transfer request initiation + */ +const postRequestToPayTransfer = async (ctx) => { + try { + // this requires a multi-stage sequence with the switch. + let requestToPayTransferRequest = { + ...ctx.request.body + }; + + // use the merchant transfers model to execute asynchronous stages with the switch + const model = new OutboundRequestToPayTransferModel({ + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }); + + // initialize the transfer model and start it running + await model.initialize(requestToPayTransferRequest); + const response = await model.run(); + // return the result + ctx.response.status = 200; + ctx.response.body = response; + } + catch (err) { + return handleRequestToPayTransferError('postRequestToPayTransfer', err, ctx); + } +}; + /** * Handler for resuming outbound transfers in scenarios where two-step transfers are enabled * by disabling the autoAcceptQuote SDK option @@ -208,7 +354,7 @@ const putRequestToPayTransfer = async (ctx) => { ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, }); // TODO: check the incoming body to reject party or quote when requested to do so @@ -239,9 +385,10 @@ const postAccounts = async (ctx) => { try { const model = new AccountsModel({ ...ctx.state.conf, + tls: ctx.state.conf.outbound.tls, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, }); const state = { @@ -273,7 +420,7 @@ const postRequestToPay = async (ctx) => { ...ctx.state.conf, cache: ctx.state.cache, logger: ctx.state.logger, - wso2Auth: ctx.state.wso2Auth, + wso2: ctx.state.wso2, }); // initialize the transfer model and start it running @@ -294,6 +441,139 @@ const healthCheck = async (ctx) => { ctx.response.body = ''; }; +const postAuthorizations = async (ctx) => { + try { + // prepare request + const authorizationsRequest = { + ...ctx.request.body + }; + + // prepare config + const modelConfig = { + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }; + + const cacheKey = `post_authorizations_${authorizationsRequest.transactionRequestId}`; + + // use the authorizations model to execute asynchronous stages with the switch + const model = await OutboundAuthorizationsModel.create(authorizationsRequest, cacheKey, modelConfig); + + // run model's workflow + const response = await model.run(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + + } catch(err) { + return handleAuthorizationsError('postAuthorizations', err, ctx); + } +}; + +const getThirdpartyRequestsTransactions = async (ctx) => { + try { + // prepare request + const thirdpartyRequestsTransactionRequest = { + ...ctx.request.body, + currentState: 'getTransaction', + transactionRequestId: ctx.state.path.params.transactionRequestId, + }; + + // prepare config + const modelConfig = { + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }; + + const cacheKey = `get_thirdparty_requests_transactions_${thirdpartyRequestsTransactionRequest}`; + + // use the thirdparty requests transaction model to execute asynchronous stages with the switch + const model = await OutboundThirdpartyTransactionModel.create(thirdpartyRequestsTransactionRequest, cacheKey, modelConfig); + + // run model's workflow + const response = await model.run(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + + } catch(err) { + return handleThirdpartyRequestsTransactionsError('getThirdpartyRequestsTransaction', err, ctx); + } +}; + +const postThirdpartyRequestsTransactions = async (ctx) => { + try { + // prepare request + const thirdpartyRequestsTransactionRequest = { + ...ctx.request.body, + currentState: 'postTransaction', + }; + + // prepare config + const modelConfig = { + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }; + + const cacheKey = `post_thirdparty_requests_transactions_${ctx.request.body.transactionRequestId}`; + + // use the thirdparty requests transaction model to execute asynchronous stages with the switch + const model = await OutboundThirdpartyTransactionModel.create(thirdpartyRequestsTransactionRequest, cacheKey, modelConfig); + + // run model's workflow + const response = await model.run(); + + // return the result + ctx.response.status = 200; + ctx.response.body = response; + + } catch(err) { + return handleThirdpartyRequestsTransactionsError('postThirdpartyRequestsTransaction', err, ctx); + } +}; + +const getPartiesByTypeAndId = async (ctx) => { + const type = ctx.state.path.params.Type; + const id = ctx.state.path.params.ID; + const subId = ctx.state.path.params.SubId; + + try { + // prepare config + const modelConfig = { + ...ctx.state.conf, + cache: ctx.state.cache, + logger: ctx.state.logger, + wso2: ctx.state.wso2, + }; + + const cacheKey = PartiesModel.generateKey(type, id, subId); + + // use the parties model to execute asynchronous stages with the switch + const model = await PartiesModel.create({}, cacheKey, modelConfig); + + // run model's workflow + const response = await model.run(type, id, subId); + + // return the result + if (response.errorInformation) { + ctx.response.status = 404; + } else { + ctx.response.status = 200; + } + ctx.response.body = response; + } catch (err) { + return handleRequestPartiesInformationError('getPartiesByTypeAndId', err, ctx); + } +}; + module.exports = { '/': { get: healthCheck @@ -305,6 +585,18 @@ module.exports = { get: getTransfers, put: putTransfers }, + '/bulkTransfers': { + post: postBulkTransfers + }, + '/bulkTransfers/{bulkTransferId}': { + get: getBulkTransfers, + }, + '/bulkQuotes': { + post: postBulkQuotes, + }, + '/bulkQuotes/{bulkQuoteId}': { + get: getBulkQuoteById, + }, '/accounts': { post: postAccounts }, @@ -317,4 +609,19 @@ module.exports = { '/requestToPayTransfer/{requestToPayTransactionId}': { put: putRequestToPayTransfer }, + '/authorizations': { + post: postAuthorizations + }, + '/thirdpartyRequests/transactions/{transactionRequestId}': { + get: getThirdpartyRequestsTransactions + }, + '/thirdpartyRequests/transactions': { + post: postThirdpartyRequestsTransactions + }, + '/parties/{Type}/{ID}': { + get: getPartiesByTypeAndId + }, + '/parties/{Type}/{ID}/{SubId}': { + get: getPartiesByTypeAndId + } }; diff --git a/src/OutboundServer/index.js b/src/OutboundServer/index.js index 8a91eb3d2..ea597ed5c 100644 --- a/src/OutboundServer/index.js +++ b/src/OutboundServer/index.js @@ -15,10 +15,9 @@ const koaBody = require('koa-body'); const yaml = require('js-yaml'); const fs = require('fs'); const path = require('path'); +const EventEmitter = require('events'); const { WSO2Auth } = require('@mojaloop/sdk-standard-components'); -const { Logger, Transports } = require('@internal/log'); -const Cache = require('@internal/cache'); const Validate = require('@internal/validate'); const router = require('@internal/router'); @@ -27,112 +26,106 @@ const middlewares = require('./middlewares'); const endpointRegex = /\/.*/g; -class OutboundServer { - constructor(conf) { - this._conf = conf; - this._api = null; - this._server = null; - this._logger = null; - } - - async setupApi() { +class OutboundApi extends EventEmitter { + constructor(conf, logger, cache, validator) { + super({ captureExceptions: true }); + this._logger = logger; this._api = new Koa(); - this._logger = await this._createLogger(); - - this._cache = await this._createCache(); - - const specPath = path.join(__dirname, 'api.yaml'); - const apiSpecs = yaml.load(fs.readFileSync(specPath)); - const validator = new Validate(); - await validator.initialise(apiSpecs); + this._conf = conf; + this._cache = cache; - this._wso2Auth = new WSO2Auth({ - ...this._conf.wso2Auth, - logger: this._logger, - tlsCreds: this._conf.tls.outbound.mutualTLS.enabled && this._conf.tls.outbound.creds, + this._wso2 = { + auth: new WSO2Auth({ + ...this._conf.wso2.auth, + logger: this._logger, + tlsCreds: this._conf.outbound.tls.mutualTLS.enabled && this._conf.outbound.tls.creds, + }), + retryWso2AuthFailureTimes: conf.wso2.requestAuthFailureRetryTimes, + }; + this._wso2.auth.on('error', (msg) => { + this.emit('error', 'WSO2 auth error in OutboundApi', msg); }); - this._api.use(middlewares.createErrorHandler()); - - // outbound always expects application/json - this._api.use(koaBody()); - - const sharedState = { cache: this._cache, wso2Auth: this._wso2Auth, conf: this._conf }; - this._api.use(middlewares.createLogger(this._logger, sharedState)); + this._api.use(middlewares.createErrorHandler(this._logger)); + this._api.use(middlewares.createRequestIdGenerator()); + this._api.use(koaBody()); // outbound always expects application/json + this._api.use(middlewares.applyState({ cache, wso2: this._wso2, conf })); + this._api.use(middlewares.createLogger(this._logger)); //Note that we strip off any path on peerEndpoint config after the origin. //this is to allow proxy routed requests to hit any path on the peer origin //irrespective of any base path on the PEER_ENDPOINT setting - if (this._conf.proxyConfig) { + if (conf.proxyConfig) { this._api.use(middlewares.createProxy({ - ...this._conf, - peerEndpoint: this._conf.peerEndpoint.replace(endpointRegex, ''), - proxyConfig: this._conf.proxyConfig, + ...conf, + peerEndpoint: conf.peerEndpoint.replace(endpointRegex, ''), + proxyConfig: conf.proxyConfig, logger: this._logger, - wso2Auth: this._wso2Auth, + wso2Auth: this._wso2.auth, + tls: conf.outbound.tls, })); } this._api.use(middlewares.createRequestValidator(validator)); this._api.use(router(handlers)); - - this._server = this._createServer(); - return this._server; } async start() { - await this._cache.connect(); if (!this._conf.testingDisableWSO2AuthStart) { - await this._wso2Auth.start(); - } - if (!this._conf.testingDisableServerStart) { - await new Promise((resolve) => this._server.listen(this._conf.outboundPort, resolve)); - this._logger.log(`Serving outbound API on port ${this._conf.outboundPort}`); + await this._wso2.auth.start(); } } async stop() { - if (!this._server) { - return; - } - await new Promise(resolve => this._server.close(resolve)); - this._wso2Auth.stop(); - await this._cache.disconnect(); - console.log('outbound shut down complete'); + this._wso2.auth.stop(); } - async _createCache() { - const transports = await Promise.all([Transports.consoleDir()]); - const logger = new Logger({ - context: { - app: 'mojaloop-sdk-outboundCache' - }, - space: this._conf.logIndent, - transports, + callback() { + return this._api.callback(); + } +} + +class OutboundServer extends EventEmitter { + constructor(conf, logger, cache) { + super({ captureExceptions: true }); + this._validator = new Validate(); + this._conf = conf; + this._logger = logger; + this._server = null; + this._api = new OutboundApi( + conf, + this._logger.push({ component: 'api' }), + cache, + this._validator + ); + this._api.on('error', (...args) => { + this.emit('error', ...args); }); + this._server = http.createServer(this._api.callback()); + } - const cacheConfig = { - ...this._conf.cacheConfig, - logger - }; + async start() { + await this._api.start(); - return new Cache(cacheConfig); - } + const specPath = path.join(__dirname, 'api.yaml'); + const apiSpecs = yaml.load(fs.readFileSync(specPath)); + await this._validator.initialise(apiSpecs); - async _createLogger() { - const transports = await Promise.all([Transports.consoleDir()]); - // Set up a logger for each running server - return new Logger({ - context: { - app: 'mojaloop-sdk-outbound-api' - }, - space: this._conf.logIndent, - transports, - }); + await new Promise((resolve) => this._server.listen(this._conf.outbound.port, resolve)); + + this._logger.log(`Serving outbound API on port ${this._conf.outbound.port}`); } - _createServer() { - return http.createServer(this._api.callback()); + async stop() { + if (this._server) { + await new Promise(resolve => this._server.close(resolve)); + this._server = null; + } + if (this._api) { + await this._api.stop(); + this._api = null; + } + this._logger.log('Shut down complete'); } } diff --git a/src/OutboundServer/middlewares.js b/src/OutboundServer/middlewares.js index 22fab02d6..341cc978e 100644 --- a/src/OutboundServer/middlewares.js +++ b/src/OutboundServer/middlewares.js @@ -9,49 +9,9 @@ **************************************************************************/ -const util = require('util'); -const randomPhrase = require('@internal/randomphrase'); const { ProxyModel } = require('@internal/model'); - - -/** - * Log raw to console as a last resort - * @return {Function} - */ -const createErrorHandler = () => async (ctx, next) => { - try { - await next(); - } catch (err) { - // TODO: return a 500 here if the response has not already been sent? - console.log(`Error caught in catchall: ${err.stack || util.inspect(err, { depth: 10 })}`); - } -}; - - -/** - * Add a log context for each request, log the receipt and handling thereof - * @param logger - * @param sharedState - * @return {Function} - */ -const createLogger = (logger, sharedState) => async (ctx, next) => { - ctx.state = { - ...ctx.state, - ...sharedState, - }; - ctx.state.logger = logger.push({ request: { - id: randomPhrase(), - path: ctx.path, - method: ctx.method - }}); - ctx.state.logger.log('Request received'); - try { - await next(); - } catch (err) { - ctx.state.logger.push(err).log('Error'); - } - await ctx.state.logger.log('Request processed'); -}; +const { applyState, createErrorHandler, createLogger, createRequestIdGenerator } = + require('../InboundServer/middlewares'); /** @@ -98,6 +58,8 @@ const createProxy = (opts) => { }; module.exports = { + applyState, + createRequestIdGenerator, createErrorHandler, createLogger, createRequestValidator, diff --git a/src/TestServer/api.yaml b/src/TestServer/api.yaml index 895199143..fe0f9915b 100644 --- a/src/TestServer/api.yaml +++ b/src/TestServer/api.yaml @@ -1,6 +1,6 @@ openapi: 3.0.0 info: - version: '1.0' + version: '1.1' title: Open API for FSP Interoperability (FSPIOP) description: >- Based on API Definition.docx updated on 2018-03-13 Version 1.0. Note - The diff --git a/src/TestServer/index.js b/src/TestServer/index.js index d283e6327..f3dea01ce 100644 --- a/src/TestServer/index.js +++ b/src/TestServer/index.js @@ -9,125 +9,206 @@ **************************************************************************/ const Koa = require('koa'); +const ws = require('ws'); -const https = require('https'); const http = require('http'); const yaml = require('js-yaml'); -const fs = require('fs'); +const fs = require('fs').promises; const path = require('path'); -const { WSO2Auth } = require('@mojaloop/sdk-standard-components'); -const { Logger, Transports } = require('@internal/log'); -const Cache = require('@internal/cache'); - const Validate = require('@internal/validate'); const router = require('@internal/router'); const handlers = require('./handlers'); const middlewares = require('../InboundServer/middlewares'); -class TestServer { - constructor(conf) { - this._conf = conf; - this._api = null; - this._server = null; - this._logger = null; - } - - async setupApi() { +const getWsIp = (req) => [ + req.socket.remoteAddress, + ...( + req.headers['x-forwarded-for'] + ? req.headers['x-forwarded-for'].split(/\s*,\s*/) + : [] + ) +]; + +class TestApi { + constructor(logger, validator, cache) { this._api = new Koa(); - this._logger = await this._createLogger(); - - this._cache = await this._createCache(); - const specPath = path.join(__dirname, 'api.yaml'); - const apiSpecs = yaml.load(fs.readFileSync(specPath)); - const validator = new Validate(); - await validator.initialise(apiSpecs); - - this._wso2Auth = new WSO2Auth({ - ...this._conf.wso2Auth, - logger: this._logger, - tlsCreds: this._conf.tls.test.mutualTLS.enabled && this._conf.tls.test.creds, - }); - - this._api.use(middlewares.createErrorHandler()); + this._api.use(middlewares.createErrorHandler(logger)); this._api.use(middlewares.createRequestIdGenerator()); - const sharedState = { cache: this._cache, wso2Auth: this._wso2Auth, conf: this._conf }; - this._api.use(middlewares.createLogger(this._logger, sharedState)); + this._api.use(middlewares.applyState({ cache })); + this._api.use(middlewares.createLogger(logger)); this._api.use(middlewares.createRequestValidator(validator)); this._api.use(router(handlers)); this._api.use(middlewares.createResponseBodyHandler()); + } - this._server = this._createServer(); - return this._server; + callback() { + return this._api.callback(); + } +} + +class WsServer extends ws.Server { + constructor(logger, cache) { + super({ noServer: true }); + this._wsClients = new Map(); + this._logger = logger; + this._cache = cache; + + this.on('error', err => { + this._logger.push({ err }) + .log('Unhandled websocket error occurred. Shutting down.'); + process.exit(1); + }); + + this.on('connection', (socket, req) => { + const logger = this._logger.push({ + url: req.url, + ip: getWsIp(req), + remoteAddress: req.socket.remoteAddress, + }); + logger.log('Websocket connection received'); + this._wsClients.set(socket, req); + socket.on('close', (code, reason) => { + logger.push({ code, reason }).log('Websocket connection closed'); + this._wsClients.delete(socket); + }); + }); } async start() { - await this._cache.connect(); - if (!this._conf.testingDisableWSO2AuthStart) { - await this._wso2Auth.start(); - } - if (!this._conf.testingDisableServerStart) { - await new Promise((resolve) => this._server.listen(this._conf.testPort, resolve)); - this._logger.log(`Serving test API on port ${this._conf.testPort}`); - } + await this._cache.subscribe(this._cache.EVENT_SET, this._handleCacheKeySet.bind(this)); } + // Close the server then wait for all the client sockets to close async stop() { - if (!this._server) { + await new Promise(resolve => this.close(resolve)); + // If we don't wait for all clients to close before shutting down, the socket close + // handlers will be called after we return from this function, resulting in behaviour + // occurring after the server tells the user it has shutdown. + await Promise.all([...this._wsClients.keys()].map(socket => + new Promise(resolve => socket.on('close', resolve)) + )); + } + + // Send the received notification to subscribers where appropriate. + async _handleCacheKeySet(channel, key, id) { + const logger = this._logger.push({ key }); + logger.push({ channel, id }).log('Received Redis keyevent notification'); + + // Only notify clients of callback and request keyevents, as we don't want to encourage + // dependency on unintended behaviour (i.e. create a proxy Redis client by sending all + // keyevent notifications to the client). Some of this is implicitly performed later in + // this method, but we use the root path `/` to enable clients to subscribe to all events, + // so we filter them here. + const allowedPrefixes = [this._cache.CALLBACK_PREFIX, this._cache.REQUEST_PREFIX]; + if (!allowedPrefixes.some((prefix) => key.startsWith(prefix))) { + logger.push({ allowedPrefixes }) + .log('Notification not of allowed message type. Ignored.'); return; } - await new Promise(resolve => this._server.close(resolve)); - this._wso2Auth.stop(); - await this._cache.disconnect(); - console.log('api shut down complete'); + + // Map urls to callback prefixes. For example, as a user of this service, if I want to + // subscribe to Redis keyevent notifications with the prefix this._cache.REQUEST_PREFIX (at + // the time of writing, that's 'request_') then I'll connect to `ws://this-server/requests`. + // The map here defines that mapping, and exists to decouple the interface (the url) from + // the implementation (the "callback prefix"). + const endpoints = { + REQUEST: '/requests', + CALLBACK: '/callbacks', + }; + const urlToMsgPrefixMap = new Map([ + [endpoints.REQUEST, this._cache.REQUEST_PREFIX], + [endpoints.CALLBACK, this._cache.CALLBACK_PREFIX], + ]); + let keyData; // declare outside the loop here, then retrieve at most once + let keyDataStr; + for (let [socket, req] of this._wsClients) { + // If + // - the url is the catch-all root (i.e. `ws://this-server/`), or + // - the url corresponds (via urlToMsgPrefixMap) to the message prefix for this + // message. E.g. if the url is /callbacks and the key is + // `${this._cache.CALLBACK_PREFIX}whatever`, or + // - the url matches the key, e.g. we replace the url prefix with the key prefix and + // obtain a match. E.g. the url is /callbacks/hello and the key is callback_hello. + // send the message to the client. + const prefix = urlToMsgPrefixMap.get(req.url); + const urlMatchesPrefix = urlToMsgPrefixMap.has(req.url) && key.startsWith(prefix); + const urlMatchesKey = + req.url.replace(new RegExp(`^${endpoints.REQUEST}/`), this._cache.REQUEST_PREFIX) === key || + req.url.replace(new RegExp(`^${endpoints.CALLBACK}/`), this._cache.CALLBACK_PREFIX) === key; + if (req.url === '/' || urlMatchesPrefix || urlMatchesKey) { + if (!keyData || !keyDataStr) { + // Strip off the prefix and send the user the id + const requestId = [...urlToMsgPrefixMap.values()].reduce( + (key, prefix) => key.replace(new RegExp(`^${prefix}`), ''), + key + ); + keyData = await this._cache.get(key); + keyDataStr = JSON.stringify({ + ...keyData, + id: requestId, + }); + } + this._logger + .push({ + url: req.url, + key, + ip: getWsIp(req), + value: keyData, + prefix, + }) + .log('Pushing notification to subscribed client'); + socket.send(keyDataStr); + } + } } +} - async _createLogger() { - const transports = await Promise.all([Transports.consoleDir()]); - // Set up a logger for each running server - return new Logger({ - context: { - app: 'mojaloop-sdk-test-api' - }, - space: this._conf.logIndent, - transports, - }); +class TestServer { + constructor({ port, logger, cache }) { + this._port = port; + this._logger = logger; + this._validator = new Validate(); + this._api = new TestApi(this._logger.push({ component: 'api' }), this._validator, cache); + this._server = http.createServer(this._api.callback()); + // TODO: why does this appear to need to be called after creating this._server (try reorder + // it then run the tests) + this._wsapi = new WsServer(this._logger.push({ component: 'websocket-server' }), cache); } - async _createCache() { - const transports = await Promise.all([Transports.consoleDir()]); - const logger = new Logger({ - context: { - app: 'mojaloop-sdk-inboundCache' - }, - space: this._conf.logIndent, - transports, + async start() { + if (this._server.listening) { + return; + } + const fileData = await fs.readFile(path.join(__dirname, 'api.yaml')); + await this._validator.initialise(yaml.load(fileData)); + + await this._wsapi.start(); + + this._server.on('upgrade', (req, socket, head) => { + this._wsapi.handleUpgrade(req, socket, head, (ws) => + this._wsapi.emit('connection', ws, req)); }); - const cacheConfig = { - ...this._conf.cacheConfig, - logger - }; + await new Promise((resolve) => this._server.listen(this._port, resolve)); - return new Cache(cacheConfig); + this._logger.log(`Serving test API on port ${this._port}`); } - _createServer() { - let server; - // If config specifies TLS, start an HTTPS server; otherwise HTTP - if (this._conf.tls.test.mutualTLS.enabled) { - const testHttpsOpts = { - ...this._conf.tls.test.creds, - requestCert: true, - rejectUnauthorized: true // no effect if requestCert is not true - }; - server = https.createServer(testHttpsOpts, this._api.callback()); - } else { - server = http.createServer(this._api.callback()); + async stop() { + if (this._wsapi) { + this._logger.log('Shutting down websocket server'); + this._wsapi.stop(); + this._wsapi = null; + } + if (this._server) { + this._logger.log('Shutting down http server'); + await new Promise(resolve => this._server.close(resolve)); + this._server = null; } - return server; + this._logger.log('Test server shutdown complete'); } } diff --git a/src/config.js b/src/config.js index 9a046ba58..272cbd942 100644 --- a/src/config.js +++ b/src/config.js @@ -21,18 +21,46 @@ function getFileContent(path) { return fs.readFileSync(path); } +/** + * Gets Resources versions from enviromental variable RESOURCES_VERSIONS + * should be string in format: "resouceOneName=1.0,resourceTwoName=1.1" + */ +function getVersionFromConfig (resourceString) { + const resourceVersionMap = {}; + resourceString + .split(',') + .forEach(e => e.split('=') + .reduce((p, c) => { + resourceVersionMap[p] = { + contentVersion: c, + acceptVersion: c.split('.')[0], + }; + })); + return resourceVersionMap; +} + +function parseResourceVersions (resourceString) { + if (!resourceString) return {}; + const resourceFormatRegex = /(([A-Za-z])\w*)=([0-9]+).([0-9]+)([^;:|],*)/g; + const noSpResources = resourceString.replace(/\s/g,''); + if (!resourceFormatRegex.test(noSpResources)) { + throw new Error('Resource versions format should be in format: "resouceOneName=1.0,resourceTwoName=1.1"'); + } + return getVersionFromConfig(noSpResources); +} + const env = from(process.env, { asFileContent: (path) => getFileContent(path), asFileListContent: (pathList) => pathList.split(',').map((path) => getFileContent(path)), asYamlConfig: (path) => yaml.load(getFileContent(path)), + asResourceVersions: (resourceString) => parseResourceVersions(resourceString), }); module.exports = { - inboundPort: env.get('INBOUND_LISTEN_PORT').default('4000').asPortNumber(), - outboundPort: env.get('OUTBOUND_LISTEN_PORT').default('4001').asPortNumber(), - testPort: env.get('TEST_LISTEN_PORT').default('4002').asPortNumber(), - tls: { - inbound: { + __parseResourceVersion: parseResourceVersions, + inbound: { + port: env.get('INBOUND_LISTEN_PORT').default('4000').asPortNumber(), + tls: { mutualTLS: { enabled: env.get('INBOUND_MUTUAL_TLS_ENABLED').default('false').asBool(), }, @@ -42,7 +70,10 @@ module.exports = { key: env.get('IN_SERVER_KEY_PATH').asFileContent(), }, }, - outbound: { + }, + outbound: { + port: env.get('OUTBOUND_LISTEN_PORT').default('4001').asPortNumber(), + tls: { mutualTLS: { enabled: env.get('OUTBOUND_MUTUAL_TLS_ENABLED').default('false').asBool(), }, @@ -52,21 +83,17 @@ module.exports = { key: env.get('OUT_CLIENT_KEY_PATH').asFileContent(), }, }, - test: { - mutualTLS: { - enabled: env.get('TEST_MUTUAL_TLS_ENABLED').default('false').asBool(), - }, - creds: { - ca: env.get('TEST_CA_CERT_PATH').asFileListContent(), - cert: env.get('TEST_CLIENT_CERT_PATH').asFileContent(), - key: env.get('TEST_CLIENT_KEY_PATH').asFileContent(), - }, - }, + }, + test: { + port: env.get('TEST_LISTEN_PORT').default('4002').asPortNumber(), }, peerEndpoint: env.get('PEER_ENDPOINT').required().asString(), - alsEndpoint: env.get('ALS_ENDPOINT_HOST').asString(), + alsEndpoint: env.get('ALS_ENDPOINT').asString(), quotesEndpoint: env.get('QUOTES_ENDPOINT').asString(), + bulkQuotesEndpoint: env.get('BULK_QUOTES_ENDPOINT').asString(), + transactionRequestsEndpoint: env.get('TRANSACTION_REQUESTS_ENDPOINT').asString(), transfersEndpoint: env.get('TRANSFERS_ENDPOINT').asString(), + bulkTransfersEndpoint: env.get('BULK_TRANSFERS_ENDPOINT').asString(), backendEndpoint: env.get('BACKEND_ENDPOINT').required().asString(), dfspId: env.get('DFSP_ID').default('mojaloop').asString(), @@ -79,6 +106,7 @@ module.exports = { autoAcceptR2PBusinessQuotes: env.get('AUTO_ACCEPT_R2P_BUSINESS_QUOTES').default('false').asBool(), autoAcceptR2PDeviceQuotes: env.get('AUTO_ACCEPT_R2P_DEVICE_QUOTES').default('true').asBool(), autoAcceptR2PDeviceOTP: env.get('AUTO_ACCEPT_R2P_DEVICE_OTP').default('false').asBool(), + autoAcceptParticipantsPut: env.get('AUTO_ACCEPT_PARTICIPANTS_PUT').default('false').asBool(), /* TODO: high-risk transactions can require additional clearing check */ // enableClearingCheck: env.get('ENABLE_CLEARING_CHECK').default('false').asBool(), @@ -105,12 +133,15 @@ module.exports = { clientSecret: env.get('OAUTH_TOKEN_ENDPOINT_CLIENT_SECRET').asString(), listenPort: env.get('OAUTH_TOKEN_ENDPOINT_LISTEN_PORT').asPortNumber(), }, - wso2Auth: { - staticToken: env.get('WSO2_BEARER_TOKEN').asString(), - tokenEndpoint: env.get('OAUTH_TOKEN_ENDPOINT').asString(), - clientKey: env.get('OAUTH_CLIENT_KEY').asString(), - clientSecret: env.get('OAUTH_CLIENT_SECRET').asString(), - refreshSeconds: env.get('OAUTH_REFRESH_SECONDS').default('60').asIntPositive(), + wso2: { + auth: { + staticToken: env.get('WSO2_BEARER_TOKEN').asString(), + tokenEndpoint: env.get('OAUTH_TOKEN_ENDPOINT').asString(), + clientKey: env.get('OAUTH_CLIENT_KEY').asString(), + clientSecret: env.get('OAUTH_CLIENT_SECRET').asString(), + refreshSeconds: env.get('OAUTH_REFRESH_SECONDS').default('60').asIntPositive(), + }, + requestAuthFailureRetryTimes: env.get('WSO2_AUTH_FAILURE_REQUEST_RETRIES').default('0').asIntPositive(), }, rejectExpiredQuoteResponses: env.get('REJECT_EXPIRED_QUOTE_RESPONSES').default('false').asBool(), rejectTransfersOnExpiredQuotes: env.get('REJECT_TRANSFERS_ON_EXPIRED_QUOTES').default('false').asBool(), @@ -120,7 +151,7 @@ module.exports = { logIndent: env.get('LOG_INDENT').default('2').asIntPositive(), - allowTransferWithoutQuote: env.get('allowTransferWithoutQuote').default('false').asBool(), + allowTransferWithoutQuote: env.get('ALLOW_TRANSFER_WITHOUT_QUOTE').default('false').asBool(), // for outbound transfers, allows an extensionList item in an error respone to be used instead // of the primary error code when setting the statusCode property on the synchronous response @@ -129,4 +160,8 @@ module.exports = { outboundErrorStatusCodeExtensionKey: env.get('OUTBOUND_ERROR_STATUSCODE_EXTENSION_KEY').asString(), proxyConfig: env.get('PROXY_CONFIG_PATH').asYamlConfig(), + reserveNotification: env.get('RESERVE_NOTIFICATION').default('false').asBool(), + // resourceVersions config should be string in format: "resouceOneName=1.0,resourceTwoName=1.1" + resourceVersions: env.get('RESOURCE_VERSIONS').default('').asResourceVersions(), + enablePISPMode: env.get('ENABLE_PISP_MODE').default('false').asBool() }; diff --git a/src/index.js b/src/index.js index 14758c57d..06ef71d54 100644 --- a/src/index.js +++ b/src/index.js @@ -8,9 +8,13 @@ * James Bush - james.bush@modusbox.com * **************************************************************************/ + 'use strict'; +const { hostname } = require('os'); const config = require('./config'); +const EventEmitter = require('events'); + const InboundServer = require('./InboundServer'); const OutboundServer = require('./OutboundServer'); const OAuthTestServer = require('./OAuthTestServer'); @@ -23,62 +27,70 @@ const OutboundServerMiddleware = require('./OutboundServer/middlewares.js'); const Router = require('@internal/router'); const Validate = require('@internal/validate'); const RandomPhrase = require('@internal/randomphrase'); -const Log = require('@internal/log'); const Cache = require('@internal/cache'); +const { Logger } = require('@mojaloop/sdk-standard-components'); /** * Class that creates and manages http servers that expose the scheme adapter APIs. */ -class Server { - constructor(conf) { +class Server extends EventEmitter { + constructor(conf, logger) { + super({ captureExceptions: true }); this.conf = conf; - this.inboundServer = null; - this.outboundServer = null; - this.oauthTestServer = null; - this.testServer = null; - } + this.logger = logger; + this.cache = new Cache({ + ...conf.cacheConfig, + logger: this.logger.push({ component: 'cache' }), + enableTestFeatures: conf.enableTestFeatures, + }); + + this.inboundServer = new InboundServer( + this.conf, + this.logger.push({ app: 'mojaloop-sdk-inbound-api' }), + this.cache + ); + this.inboundServer.on('error', (...args) => { + this.logger.push({ args }).log('Unhandled error in Inbound Server'); + this.emit('error', 'Unhandled error in Inbound Server'); + }); + + this.outboundServer = new OutboundServer( + this.conf, + this.logger.push({ app: 'mojaloop-sdk-outbound-api' }), + this.cache + ); + this.outboundServer.on('error', (...args) => { + this.logger.push({ args }).log('Unhandled error in Outbound Server'); + this.emit('error', 'Unhandled error in Outbound Server'); + }); - async start() { - this.inboundServer = new InboundServer(this.conf); - this.outboundServer = new OutboundServer(this.conf); this.oauthTestServer = new OAuthTestServer({ clientKey: this.conf.oauthTestServer.clientKey, clientSecret: this.conf.oauthTestServer.clientSecret, port: this.conf.oauthTestServer.listenPort, - logIndent: this.conf.logIndent, + logger: this.logger.push({ app: 'mojaloop-sdk-oauth-test-server' }), }); - this.testServer = new TestServer(this.conf); - - await Promise.all([ - this._startInboundServer(), - this._startOutboundServer(), - this._startOAuthTestServer(), - this._startTestServer(), - ]); - } - - async _startTestServer() { - if (this.conf.enableTestFeatures) { - await this.testServer.setupApi(); - await this.testServer.start(); - } - } - async _startInboundServer() { - await this.inboundServer.setupApi(); - await this.inboundServer.start(); + this.testServer = new TestServer({ + port: this.conf.test.port, + logger: this.logger.push({ app: 'mojaloop-sdk-test-api' }), + cache: this.cache, + }); } - async _startOutboundServer() { - await this.outboundServer.setupApi(); - await this.outboundServer.start(); - } + async start() { + await this.cache.connect(); - async _startOAuthTestServer() { - if (this.conf.oauthTestServer.enabled) { - await this.oauthTestServer.setupApi(); - await this.oauthTestServer.start(); - } + const startTestServer = this.conf.enableTestFeatures ? this.testServer.start() : null; + const startOauthTestServer = this.conf.oauthTestServer.enabled + ? this.oauthTestServer.start() + : null; + await Promise.all([ + this.inboundServer.start(), + this.outboundServer.start(), + startTestServer, + startOauthTestServer, + ]); } stop() { @@ -96,18 +108,29 @@ if(require.main === module) { (async () => { // this module is main i.e. we were started as a server; // not used in unit test or "require" scenarios - const svr = new Server(config); + const logger = new Logger.Logger({ + context: { + // If we're running from a Mojaloop helm chart deployment, we'll have a SIM_NAME + simulator: process.env['SIM_NAME'], + hostname: hostname(), + }, + stringify: Logger.buildStringify({ space: config.logIndent }), + }); + const svr = new Server(config, logger); + svr.on('error', (err) => { + logger.push({ err }).log('Unhandled server error'); + process.exit(1); + }); // handle SIGTERM to exit gracefully process.on('SIGTERM', async () => { - console.log('SIGTERM received. Shutting down APIs...'); - + logger.log('SIGTERM received. Shutting down APIs...'); await svr.stop(); process.exit(0); }); svr.start().catch(err => { - console.log(err); + logger.push({ err }).log('Error starting server'); process.exit(1); }); })(); @@ -117,12 +140,11 @@ if(require.main === module) { // export things we want to expose e.g. for unit tests and users who dont want to use the entire // scheme adapter as a service module.exports = { - Server: Server, - InboundServerMiddleware: InboundServerMiddleware, - OutboundServerMiddleware: OutboundServerMiddleware, - Router: Router, - Validate: Validate, - RandomPhrase: RandomPhrase, - Log: Log, - Cache: Cache + Cache, + InboundServerMiddleware, + OutboundServerMiddleware, + RandomPhrase, + Router, + Server, + Validate, }; diff --git a/src/lib/cache/cache.js b/src/lib/cache/cache.js index 382f3ef0f..55167a13e 100644 --- a/src/lib/cache/cache.js +++ b/src/lib/cache/cache.js @@ -13,6 +13,12 @@ const util = require('util'); const redis = require('redis'); +const CONN_ST = { + CONNECTED: 'CONNECTED', + CONNECTING: 'CONNECTING', + DISCONNECTED: 'DISCONNECTED', + DISCONNECTING: 'DISCONNECTING', +}; /** * A shared cache abstraction over a REDIS distributed key/value store @@ -30,6 +36,9 @@ class Cache { // a redis connection to handle get, set and publish operations this._client = null; + // connection/disconnection logic + this._connectionState = CONN_ST.DISCONNECTED; + // a redis connection to handle subscribe operations and published message routing // Note that REDIS docs suggest a client that is in SUBSCRIBE mode // should not have any other commands executed against it. @@ -43,7 +52,6 @@ class Cache { this._callbackId = 0; } - /** * Connects to a redis server and waits for ready events * Note: We create two connections. One for get, set and publish commands @@ -52,26 +60,78 @@ class Cache { * See: https://redis.io/topics/pubsub */ async connect() { - if (this._connected) { - throw new Error('already connected'); + switch(this._connectionState) { + case CONN_ST.CONNECTED: + return; + case CONN_ST.CONNECTING: + await this._inProgressConnection; + return; + case CONN_ST.DISCONNECTED: + break; + case CONN_ST.DISCONNECTING: + // TODO: should this be an error? + // If we're disconnecting, we'll let that finish first + await this._inProgressDisconnection; + break; } - this._connected = true; - this._client = await this._getClient(); - this._subscriptionClient = await this._getClient(); + this._connectionState = CONN_ST.CONNECTING; + this._inProgressConnection = Promise.all([this._getClient(), this._getClient()]); + [this._client, this._subscriptionClient] = await this._inProgressConnection; // hook up our sub message handler this._subscriptionClient.on('message', this._onMessage.bind(this)); + + if (this._config.enableTestFeatures) { + this.setTestMode(true); + } + + this._inProgressConnection = null; + this._connectionState = CONN_ST.CONNECTED; + } + + /** + * Configure Redis to emit keyevent events. This corresponds to the application test mode, and + * enables us to listen for changes on callback_* and request_* keys. + * Docs: https://redis.io/topics/notifications + */ + async setTestMode(enable) { + // See for modes: https://redis.io/topics/notifications#configuration + // This mode, 'Es$' is: + // E Keyevent events, published with __keyevent@__ prefix. + // s Set commands + // $ String commands + const mode = enable ? 'Es$' : ''; + this._logger + .push({ 'notify-keyspace-events': mode }) + .log('Configuring Redis to emit keyevent events'); + this._client.config('SET', 'notify-keyspace-events', mode); } async disconnect() { - if (!this._connected) { - return; + switch(this._connectionState) { + case CONN_ST.CONNECTED: + break; + case CONN_ST.CONNECTING: + // TODO: should this be an error? + // If we're connecting, we'll let that finish first + await this._inProgressConnection; + break; + case CONN_ST.DISCONNECTED: + return; + case CONN_ST.DISCONNECTING: + await this._inProgressDisconnection; + return; } - await Promise.all([ + this._connectionState = CONN_ST.DISCONNECTING; + this._inProgressDisconnection = Promise.all([ new Promise(resolve => this._client.quit(resolve)), new Promise(resolve => this._subscriptionClient.quit(resolve)) ]); - this._connected = false; + this._client = null; + this._subscriptionClient = null; + await this._inProgressDisconnection; + this._inProgressDisconnection = null; + this._connectionState = CONN_ST.DISCONNECTED; } @@ -149,7 +209,13 @@ class Cache { // call the callback with the channel name, message and callbackId... // ...(which is useful for unsubscribe) - this._callbacks[channel][k](channel, msg, k); + try { + this._callbacks[channel][k](channel, msg, k); + } catch (err) { + this._logger + .push({ callbackId: k, err }) + .log('Unhandled error in cache subscription handler'); + } }); } } @@ -261,5 +327,10 @@ class Cache { } } +// Define constants on the prototype, but prevent a user of the cache from overwriting them for all +// instances +Object.defineProperty(Cache.prototype, 'CALLBACK_PREFIX', { value: 'callback_', writable: false }); +Object.defineProperty(Cache.prototype, 'REQUEST_PREFIX', { value: 'request_', writable: false }); +Object.defineProperty(Cache.prototype, 'EVENT_SET', { value: '__keyevent@0__:set', writable: false }); module.exports = Cache; diff --git a/src/lib/check/index.js b/src/lib/check/index.js new file mode 100644 index 000000000..9bc053eb7 --- /dev/null +++ b/src/lib/check/index.js @@ -0,0 +1,25 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Matt Kingston - matt.kingston@modusbox.com * + **************************************************************************/ + +// This module maps all methods on Node assert to non-throwing "check" functions which return true +// if the assertion succeeded and false otherwise + +const assert = require('assert').strict; + +module.exports = Object.fromEntries( + Object.entries(assert).map(([k, f]) => [k, (...args) => { + try { + f.bind(assert)(...args); + return true; + } catch (err) { + return false; + } + }]) +); diff --git a/src/lib/check/package.json b/src/lib/check/package.json new file mode 100644 index 000000000..e6ef9809e --- /dev/null +++ b/src/lib/check/package.json @@ -0,0 +1,12 @@ +{ + "name": "check", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/src/lib/log/log.js b/src/lib/log/log.js deleted file mode 100644 index 679b5a5cb..000000000 --- a/src/lib/log/log.js +++ /dev/null @@ -1,216 +0,0 @@ -/************************************************************************** - * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * - * * - * This file is made available under the terms of the license agreement * - * specified in the corresponding source code repository. * - * * - * ORIGINAL AUTHOR: * - * Matt Kingston - matt.kingston@modusbox.com * - **************************************************************************/ - -'use strict'; - - -// An immutable structured logger. It uses JSON.stringify to -// stringify any arguments. -// TODO: either make this callable, or remove this text. See https://stackoverflow.com/questions/36871299/how-to-extend-function-with-es6-classes -// It is callable, such that: -// (new require('./log'))('stuff I want logged'); -// will print: -// { "msg": "stuff I want logged" } -// -// JSON.stringify blocks the event loop. At the time of writing, performance/responsiveness were -// not requirements of this module. If this is later required, see the discussion here for -// solutions: https://nodejs.org/en/docs/guides/dont-block-the-event-loop/. This may necessitate -// either a print queue, or a print sequence number to help identify print order. This could be -// optional in the constructor options. - -// This logger could be considered immutable for the following two reasons. However, it could -// retain that property and simply return a new logger from a 'pop' or 'replace' method. -// 1) At the time of writing, this class does not implement any mechanism to remove any logging -// context. This was a conscious decision to enable better reasoning about logging. "This logger is -// derived from that logger, therefore the context must be a non-strict superset of the context -// of the parent logger". However, this is something of an experiment, and at some time in the -// future may be considered an impediment, or redundant, and the aforementioned mechanism (e.g. -// a pop method) may be added. -// 2) No 'replace' method (or method for overwriting logged context) has been implemented. This is -// for the same reason no 'pop' method has been implemented. - -// TODO: -// - support log levels? just support conditional logging? (i.e. if the .level property is "in -// {x,y,z}" then log) -// - just call the logger to add a message property and log that- pass all arguments to util.format -// - support logging to a single line -// - support 'verbose', 'debug', 'warn', 'error', 'trace', 'info', 'fatal' methods? -// - support env var config? - -// TODO: -// Is it possible to pretty-print log messages or strings containing new-line characters? For -// example, instead of printing the '\n' characters in a stack-trace, actually printing the -// new-line characters. Is that possible and/or worthwhile? - -const util = require('util'); - -const contextSym = Symbol('Logger context symbol'); - - -/** - * Returns a function that removes circular references and some custom - * interpretations of certain types of objects - * - * @returns {function} - */ -const getReplacer = () => { - const seen = new WeakSet(); - return (key, value) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[Circular Reference]'; - } - seen.add(value); - } - - return replaceOutput(key, value); - }; -}; - -const replaceOutput = (key, value) => { - if (value instanceof Error) { - return Object.getOwnPropertyNames(value).reduce((acc, key) => ({ ...acc, [key]: value[key] }), {}); - } - if (value instanceof RegExp) { - return value.toString(); - } - if (value instanceof Function) { - return `[Function: ${value.name || 'anonymous'}]`; - } - return value; -}; - - -class Logger { - // space - // String | Number - // The default formatting to be supplied to the JSON.stringify method. Examples include the - // string '\t' to indent with a tab and the number 4 to indent with four spaces. The default, - // undefined, will not break lines. - // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Parameters - // printTimestamp - // Boolean - // Whether to print a timestamp. - // timestampFmt - // Function - // A function that accepts a Date object and produces a timestamp string. - // transports - // Array of functions - // Each function will be supplied with arguments (String msg, Date timestamp) for each log - // event. - // context - // Object - // Context data to preload in the logger. Example: { path: '/users', method: 'GET' } - // This logger and all loggers derived from it (with the push method) will print this context - // If any reserved keys exist in the new object, an error will be thrown. - constructor({ - context = {}, - space, - printTimestamp = true, - timestampFmt = ts => ts.toISOString(), - transports = [] - } = {}) { - this.opts = {}; - this.configure({ space, printTimestamp, timestampFmt }); - if (this.opts.printTimestamp && 'timestamp' in context) { - throw new Error('\'timestamp\' is a reserved logger key when providing \'timestamp: true\' to the constructor'); - } - if ('msg' in context) { - throw new Error('\'msg\' is a reserved logger key'); - } - this.opts.transports = transports; - this[contextSym] = context; - } - - // Update logger configuration. - // opts - // Object. May contain any of .space, .printTimestamp, .timestampFmt - // See constructor comment for details - configure(opts) { - // TODO: check whether printTimestamp has gone from false to true, and whether the - // timestamp key exists in our context - // TODO: should we check whether a timestamp format function has been provided, but - // printtimestamp has been set to false? - this.opts = { ...this.opts, ...opts }; - } - - // space - // String | Number - // The default formatting to be supplied to the JSON.stringify method. Examples include the - // string '\t' to indent with a tab and the number 4 to indent with four spaces. The default, - // undefined, will not break lines. - setSpace(space) { - this.space = space; - } - - // Create a new logger with the same context as the current logger, and additionally any - // supplied context. - // context - // An object to log. Example: { path: '/users', method: 'GET' } - // If a key in this object already exists in this logger, an error will be thrown. - push(context) { - if (!context) { - return new Logger({ ...this.opts, context: this[contextSym] }); - } - // Check none of the new context replaces any of the old context - if (-1 !== Object.keys(context).findIndex(k => Object.keys(this[contextSym]).findIndex(l => l === k) !== -1)) { - throw new Error('Key already exists in logger'); - } - return new Logger({ ...this.opts, context: { ...this[contextSym], ...context } }); - } - - // Log to transports. - // args - // Any type is acceptable. All arguments will be passed to util.format, then printed as the - // 'msg' property of the logged item. - async log(...args) { - // NOTE: if printing large strings, JSON.stringify will block the event loop. This, and - // solutions, are discussed here: - // https://nodejs.org/en/docs/guides/dont-block-the-event-loop/. - // At the time of writing, this was considered unlikely to be a problem, as this - // implementation did not have any performance requirements - const msg = args.length > 0 ? util.format(...args) : undefined; - const ts = new Date(); - let output; - if (this.opts.printTimestamp) { - output = JSON.stringify({ ...this[contextSym], msg, timestamp: this.opts.timestampFmt(ts) }, getReplacer(), this.opts.space); - } else { - output = JSON.stringify({ ...this[contextSym], msg }, getReplacer(), this.opts.space); - } - await Promise.all(this.opts.transports.map(t => t(output, ts))); - } - - async error(...args) { - await this.log({ - LOG_LEVEL: 'ERROR', - ...args, - }); - } - - async info(...args) { - await this.log({ - LOG_LEVEL: 'INFO', - ...args, - }); - } - - async debug(...args) { - await this.log({ - LOG_LEVEL: 'DEBUG', - ...args, - }); - } -} - - -module.exports = { - Logger, - Transports: require('./transports') -}; diff --git a/src/lib/log/package.json b/src/lib/log/package.json deleted file mode 100644 index 9ea30a351..000000000 --- a/src/lib/log/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@internal/log", - "version": "0.0.1", - "description": "An immutable strutured logger", - "main": "log.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Matt Kingston, ModusBox Inc", - "license": "Apache-2.0", - "licenses": [ - { - "type": "Apache-2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0" - } - ], - "dependencies": { - "sqlite": "^3.0.1" - } -} diff --git a/src/lib/log/transports.js b/src/lib/log/transports.js deleted file mode 100644 index d1b5fc253..000000000 --- a/src/lib/log/transports.js +++ /dev/null @@ -1,61 +0,0 @@ -/************************************************************************** - * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * - * * - * This file is made available under the terms of the license agreement * - * specified in the corresponding source code repository. * - * * - * ORIGINAL AUTHOR: * - * Matt Kingston - matt.kingston@modusbox.com * - **************************************************************************/ - -'use strict'; - - -const fs = require('fs'); -// TODO: consider: https://github.com/JoshuaWise/better-sqlite3 -const sqlite = require('sqlite'); - -const consoleDir = () => msg => { - console.dir(JSON.parse(msg), { depth: Infinity, colors: true }); -}; - -const stdout = () => msg => { - process.stdout.write(msg); - process.stdout.write('\n'); -}; - -const file = path => { - // TODO: check this isn't in object mode. See here for more: - // https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback - const stream = fs.createWriteStream(path, { flags: 'a' }); - // TODO: when the filesystem fills up? - return async msg => new Promise(res => { - if (!stream.write(msg)) { - stream.once('drain', res); - } else { - res(); - } - }); -}; - -const sqliteTransport = async path => { - // TODO: enable db object cache? https://github.com/mapbox/node-sqlite3/wiki/Caching - const db = await sqlite.open(path); - await db.run('CREATE TABLE IF NOT EXISTS log(timestamp TEXT, entry TEXT)'); - await db.run('CREATE INDEX IF NOT EXISTS log_timestamp_index ON log(timestamp)'); - // TODO: when the filesystem fills up? - // - set a maximum table size? Discard earlier entries when full? - return async ($msg, timestamp) => { - const $ts = timestamp.toISOString(); - await db.run('INSERT INTO log(timestamp, entry) VALUES ($ts, json($msg))', { $ts, $msg }); - return; - }; -}; - - -module.exports = { - stdout, - sqlite: sqliteTransport, - file, - consoleDir, -}; diff --git a/src/lib/model/AccountsModel.js b/src/lib/model/AccountsModel.js index 47f779228..36a6279a3 100644 --- a/src/lib/model/AccountsModel.js +++ b/src/lib/model/AccountsModel.js @@ -36,10 +36,10 @@ class AccountsModel { logger: this._logger, peerEndpoint: config.alsEndpoint, dfspId: config.dfspId, - tls: config.tls, + tls: config.outbound.tls, jwsSign: config.jwsSign, jwsSigningKey: config.jwsSigningKey, - wso2Auth: config.wso2Auth + wso2: config.wso2, }); } diff --git a/src/lib/model/InboundThirdpartyTransactionModel.js b/src/lib/model/InboundThirdpartyTransactionModel.js new file mode 100644 index 000000000..ab431e959 --- /dev/null +++ b/src/lib/model/InboundThirdpartyTransactionModel.js @@ -0,0 +1,106 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Sridhar Voruganti - sridhar.voruganti@modusbox.com * + **************************************************************************/ + +'use strict'; + +const { + BackendRequests, + HTTPResponseError, +} = require('@internal/requests'); + +const { + MojaloopRequests, + Errors, +} = require('@mojaloop/sdk-standard-components'); + +const shared = require('@internal/shared'); + +/** + * Models the operations required for inbound third party transaction requests + */ +class InboundThirdpartyTransactionModel { + constructor(config) { + this._logger = config.logger; + this._dfspId = config.dfspId; + + this._mojaloopRequests = new MojaloopRequests({ + logger: this._logger, + peerEndpoint: config.peerEndpoint, + alsEndpoint: config.alsEndpoint, + quotesEndpoint: config.quotesEndpoint, + transfersEndpoint: config.transfersEndpoint, + transactionRequestsEndpoint: config.transactionRequestsEndpoint, + dfspId: config.dfspId, + tls: config.outbound.tls, + jwsSign: config.jwsSign, + jwsSignPutParties: config.jwsSignPutParties, + jwsSigningKey: config.jwsSigningKey, + wso2: config.wso2, + }); + + this._backendRequests = new BackendRequests({ + logger: this._logger, + backendEndpoint: config.backendEndpoint, + dfspId: config.dfspId + }); + } + + /** + * Queries the backend API to get the authorization details and makes a + * callback to the originator with the result + */ + async postAuthorizations(authorizationsReq, sourceFspId) { + try { + //Converts a mojaloop authorizationsReq data to internal form + const internalForm = shared.mojaloopAuthorizationsReqToInternal(authorizationsReq); + + // make a call to the backend to ask for a authorizations response + const response = await this._backendRequests.getSignedChallenge(internalForm); + if (!response) { + // make an error callback to the source fsp + return 'No response from backend'; + } + // project our internal authorizations reponse into mojaloop response form + const mojaloopResponse = shared.internalAuthorizationsResponseToMojaloop(response); + // make a callback to the source fsp with the party info + return this._mojaloopRequests.putAuthorizations(authorizationsReq.transactionRequestId, mojaloopResponse, sourceFspId); + } + catch (err) { + this._logger.push({ err }).log('Error in postAuthorizations'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return await this._mojaloopRequests.putAuthorizationsError(authorizationsReq.transactionRequestId, mojaloopError, sourceFspId); + } + } + + async _handleError(err) { + let mojaloopErrorCode = Errors.MojaloopApiErrorCodes.INTERNAL_SERVER_ERROR; + if (err instanceof HTTPResponseError) { + const e = err.getData(); + if(e.res && (e.res.body || e.res.data)) { + if(e.res.body) { + try { + const bodyObj = JSON.parse(e.res.body); + mojaloopErrorCode = Errors.MojaloopApiErrorCodeFromCode(`${bodyObj.statusCode}`); + } catch(ex) { + // do nothing + this._logger.push({ ex }).log('Error parsing error message body as JSON'); + } + + } else if(e.res.data) { + mojaloopErrorCode = Errors.MojaloopApiErrorCodeFromCode(`${e.res.data.statusCode}`); + } + } + } + return new Errors.MojaloopFSPIOPError(err, null, null, mojaloopErrorCode).toApiErrorObject(); + } +} + +module.exports = InboundThirdpartyTransactionModel; diff --git a/src/lib/model/InboundTransfersModel.js b/src/lib/model/InboundTransfersModel.js index daa1558a3..3e471ebc4 100644 --- a/src/lib/model/InboundTransfersModel.js +++ b/src/lib/model/InboundTransfersModel.js @@ -22,7 +22,6 @@ const { } = require('@mojaloop/sdk-standard-components'); const shared = require('@internal/shared'); - /** * Models the operations required for performing inbound transfers */ @@ -34,6 +33,7 @@ class InboundTransfersModel { this._expirySeconds = config.expirySeconds; this._rejectTransfersOnExpiredQuotes = config.rejectTransfersOnExpiredQuotes; this._allowTransferWithoutQuote = config.allowTransferWithoutQuote; + this._reserveNotification = config.reserveNotification; this._mojaloopRequests = new MojaloopRequests({ logger: this._logger, @@ -41,11 +41,15 @@ class InboundTransfersModel { alsEndpoint: config.alsEndpoint, quotesEndpoint: config.quotesEndpoint, transfersEndpoint: config.transfersEndpoint, + bulkTransfersEndpoint: config.bulkTransfersEndpoint, + transactionRequestsEndpoint: config.transactionRequestsEndpoint, + bulkQuotesEndpoint: config.bulkQuotesEndpoint, dfspId: config.dfspId, - tls: config.tls, + tls: config.inbound.tls, jwsSign: config.jwsSign, jwsSigningKey: config.jwsSigningKey, - wso2Auth: config.wso2Auth + wso2: config.wso2, + resourceVersions: config.resourceVersions }); this._backendRequests = new BackendRequests({ @@ -57,7 +61,8 @@ class InboundTransfersModel { this._checkIlp = config.checkIlp; this._ilp = new Ilp({ - secret: config.ilpSecret + secret: config.ilpSecret, + logger: this._logger, }); } @@ -79,7 +84,7 @@ class InboundTransfersModel { authentication: 'OTP', authenticationValue: `${response.otpValue}` }, - responseType: 'ENTERED' + responseType: 'ENTERED' }; // make a callback to the source fsp with the party info return this._mojaloopRequests.putAuthorizations(transactionRequestId, mlAuthorization, sourceFspId); @@ -149,61 +154,6 @@ class InboundTransfersModel { } } - /** - * Queries details of a transfer - */ - async getTransfer(transferId, sourceFspId) { - try { - // make a call to the backend to get transfer details - const response = await this._backendRequests.getTransfers(transferId); - - if(!response) { - return 'No response from backend'; - } - - const ilpPaymentData = { - transferId: transferId, - homeTransactionId: response.homeTransactionId, - from: shared.internalPartyToMojaloopParty(response.from, response.from.fspId), - to: shared.internalPartyToMojaloopParty(response.to, response.to.fspId), - amountType: response.amountType, - currency: response.currency, - amount: response.amount, - transactionType: response.transactionType, - note: response.note, - }; - - let fulfilment; - if (this._dfspId === response.to.fspId) { - fulfilment = this._ilp.getResponseIlp(ilpPaymentData).fulfilment; - } - - // create a mojaloop transfer fulfil response - const mojaloopResponse = { - completedTimestamp: response.timestamp, - transferState: response.transferState, - fulfilment, - ...response.extensions && { - extensionList: { - extension: response.extensions, - }, - }, - }; - - // make a callback to the source fsp with the transfer fulfilment - return this._mojaloopRequests.putTransfers(transferId, mojaloopResponse, - sourceFspId); - } - catch(err) { - this._logger.push({ err }).log('Error in getTransfers'); - const mojaloopError = await this._handleError(err); - this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); - return this._mojaloopRequests.putTransfersError(transferId, - mojaloopError, sourceFspId); - } - } - - /** * Asks the backend for a response to an incoming quote request and makes a callback to the originator with * the result @@ -243,6 +193,9 @@ class InboundTransfersModel { fulfilment: fulfilment }); + // now store the quoteRespnse data against the quoteId in our cache to be sent as a response to GET /quotes/{ID} + await this._cache.set(`quoteResponse_${quoteRequest.quoteId}`, mojaloopResponse); + // make a callback to the source fsp with the quote response return this._mojaloopRequests.putQuotes(quoteRequest.quoteId, mojaloopResponse, sourceFspId); } @@ -255,6 +208,35 @@ class InboundTransfersModel { } } + /** + * This is executed as when GET /quotes/{ID} request is made to get the response of a previous POST /quotes request. + * Gets the quoteResponse from the cache and makes a callback to the originator with result + */ + async getQuoteRequest(quoteId, sourceFspId) { + try { + // Get the quoteRespnse data for the quoteId from the cache to be sent as a response to GET /quotes/{ID} + const quoteResponse = await this._cache.get(`quoteResponse_${quoteId}`); + + // If no quoteResponse is found in the cache, make an error callback to the source fsp + if (!quoteResponse) { + const err = new Error('Quote Id not found'); + const mojaloopError = await this._handleError(err, Errors.MojaloopApiErrorCodes.QUOTE_ID_NOT_FOUND); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return await this._mojaloopRequests.putQuotesError(quoteId, + mojaloopError, sourceFspId); + } + // Make a PUT /quotes/{ID} callback to the source fsp with the quote response + return this._mojaloopRequests.putQuotes(quoteId, quoteResponse, sourceFspId); + } + catch(err) { + this._logger.push({ err }).log('Error in getQuoteRequest'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return await this._mojaloopRequests.putQuotesError(quoteId, + mojaloopError, sourceFspId); + } + } + /** * Asks the backend for a response to an incoming transactoin request and makes a callback to the originator with * the result @@ -348,7 +330,7 @@ class InboundTransfersModel { // create a mojaloop transfer fulfil response const mojaloopResponse = { completedTimestamp: new Date(), - transferState: 'COMMITTED', + transferState: this._reserveNotification ? 'RESERVED' : 'COMMITTED', fulfilment: fulfilment, ...response.extensionList && { extensionList: { @@ -370,22 +352,374 @@ class InboundTransfersModel { } } - async _handleError(err) { - let mojaloopErrorCode = Errors.MojaloopApiErrorCodes.INTERNAL_SERVER_ERROR; + /** + * Queries details of a transfer + */ + async getTransfer(transferId, sourceFspId) { + try { + // make a call to the backend to get transfer details + const response = await this._backendRequests.getTransfers(transferId); - if(err instanceof HTTPResponseError) { - const e = err.getData(); - if(e.res && e.res.body) { - try { - const bodyObj = JSON.parse(e.res.body); - mojaloopErrorCode = Errors.MojaloopApiErrorCodeFromCode(`${bodyObj.statusCode}`); + if (!response) { + return 'No response from backend'; + } + + const ilpPaymentData = { + transferId: transferId, + homeTransactionId: response.homeTransactionId, + from: shared.internalPartyToMojaloopParty(response.from, response.from.fspId), + to: shared.internalPartyToMojaloopParty(response.to, response.to.fspId), + amountType: response.amountType, + currency: response.currency, + amount: response.amount, + transactionType: response.transactionType, + note: response.note, + }; + + let fulfilment; + if (this._dfspId === response.to.fspId) { + fulfilment = this._ilp.getResponseIlp(ilpPaymentData).fulfilment; + } + + // create a mojaloop transfer fulfil response + const mojaloopResponse = { + completedTimestamp: response.timestamp, + transferState: response.transferState, + fulfilment, + ...response.extensions && { + extensionList: { + extension: response.extensions, + }, + }, + }; + + // make a callback to the source fsp with the transfer fulfilment + return this._mojaloopRequests.putTransfers(transferId, mojaloopResponse, + sourceFspId); + } + catch (err) { + this._logger.push({ err }).log('Error in getTransfers'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return this._mojaloopRequests.putTransfersError(transferId, + mojaloopError, sourceFspId); + } + } + + /** + * Asks the backend for a response to an incoming bulk quotes request and makes a callback to the originator with + * the results. + */ + async bulkQuoteRequest(bulkQuoteRequest, sourceFspId) { + const { bulkQuoteId } = bulkQuoteRequest; + const fulfilments = {}; + try { + const internalForm = shared.mojaloopBulkQuotesRequestToInternal(bulkQuoteRequest); + + // make a call to the backend to ask for bulk quotes response + const response = await this._backendRequests.postBulkQuotes(internalForm); + + if (!response) { + // make an error callback to the source fsp + return 'No response from backend'; + } + + if (!response.expiration) { + const expiration = new Date().getTime() + (this._expirySeconds * 1000); + response.expiration = new Date(expiration).toISOString(); + } + + // project our internal bulk quotes response into mojaloop bulk quotes response form + const mojaloopResponse = shared.internalBulkQuotesResponseToMojaloop(response); + + // create our ILP packet and condition and tag them on to our internal quote response + bulkQuoteRequest.individualQuotes.map((quote) => { + const quoteRequest = { + transactionId: quote.transactionId, + quoteId: quote.quoteId, + payee: quote.payee, + payer: bulkQuoteRequest.payer, + transactionType: quote.transactionType, + }; + // TODO: Optimize with a HashMap + const mojaloopIndividualQuote = mojaloopResponse.individualQuoteResults.find( + (quoteResult) => quoteResult.quoteId === quote.quoteId + ); + const quoteResponse = { + transferAmount: mojaloopIndividualQuote.transferAmount, + note: mojaloopIndividualQuote.note || '', + }; + const { fulfilment, ilpPacket, condition } = this._ilp.getQuoteResponseIlp( + quoteRequest, quoteResponse); + + // mutate individual quotes in `mojaloopResponse` + mojaloopIndividualQuote.ilpPacket = ilpPacket; + mojaloopIndividualQuote.condition = condition; + + fulfilments[quote.quoteId] = fulfilment; + }); + + // now store the fulfilments and the bulk quotes data against the bulkQuoteId in our cache + await this._cache.set(`bulkQuotes_${bulkQuoteId}`, { + request: bulkQuoteRequest, + internalRequest: internalForm, + mojaloopResponse: mojaloopResponse, + response, + fulfilments + }); + + // make a callback to the source fsp with the quote response + return this._mojaloopRequests.putBulkQuotes(bulkQuoteId, mojaloopResponse, sourceFspId); + } + catch (err) { + this._logger.push({ err }).log('Error in bulkQuotesRequest'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return await this._mojaloopRequests.putBulkQuotesError(bulkQuoteId, + mojaloopError, sourceFspId); + } + } + + /** + * Queries details of a bulk quote + */ + async getBulkQuote(bulkQuoteId, sourceFspId) { + try { + // make a call to the backend to get bulk quote details + const response = await this._backendRequests.getBulkQuotes(bulkQuoteId); + + if (!response) { + return 'No response from backend'; + } + + // project our internal quote reponse into mojaloop bulk quote response form + const mojaloopResponse = shared.internalBulkQuotesResponseToMojaloop(response); + + // make a callback to the source fsp with the bulk quote response + return this._mojaloopRequests.putBulkQuotes(bulkQuoteId, mojaloopResponse, + sourceFspId); + } + catch (err) { + this._logger.push({ err }).log('Error in getBulkQuote'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return this._mojaloopRequests.putBulkQuotesError(bulkQuoteId, + mojaloopError, sourceFspId); + } + } + + /** + * Validates an incoming bulk transfer prepare request and makes a callback to the originator with + * the result + */ + async prepareBulkTransfer(bulkPrepareRequest, sourceFspId) { + try { + // retrieve bulk quote data + const bulkQuote = await this._cache.get(`bulkQuotes_${bulkPrepareRequest.bulkQuoteId}`); + + if (!bulkQuote) { + // Check whether to allow transfers without a previous quote. + if (!this._allowTransferWithoutQuote) { + throw new Error(`Corresponding bulk quotes not found for bulk transfers ${bulkPrepareRequest.bulkTransferId}`); + } + } + + // create an index of individual quote results indexed by transactionId for faster lookups + const quoteResultsByTrxId = {}; + + if (bulkQuote && bulkQuote.mojaloopResponse && bulkQuote.mojaloopResponse.individualQuoteResults) { + for (const quoteResult of bulkQuote.mojaloopResponse.individualQuoteResults) { + quoteResultsByTrxId[quoteResult.transactionId] = quoteResult; + } + } + + // transfer fulfilments + const fulfilments = {}; + + // collect errors for each transfer + let individualTransferErrors = []; + + // validate individual transfer + for (const transfer of bulkPrepareRequest.individualTransfers) { + // decode ilpPacked for this transfer to get transaction object + const transactionObject = this._ilp.getTransactionObject(transfer.ilpPacket); + + // we use the transactionId from the decoded ilpPacked in the transfer to match a corresponding quote + const quote = quoteResultsByTrxId[transactionObject.transactionId] || null; + + // calculate or retrieve fulfilments and conditions + let fulfilment = null; + let condition = null; + + if (quote) { + fulfilment = bulkQuote.fulfilments[quote.quoteId]; + condition = quote.condition; + } + else { + fulfilment = this._ilp.calculateFulfil(transfer.ilpPacket); + condition = this._ilp.calculateConditionFromFulfil(fulfilment); + } + + fulfilments[transfer.transferId] = fulfilment; + + // check incoming ILP matches our persisted values + if (this._checkIlp && (transfer.condition !== condition)) { + const transferError = this._handleError(new Error(`ILP condition in bulk transfers prepare for ${transfer.transferId} does not match quote`)); + individualTransferErrors.push({ transferId: transfer.transferId, transferError }); } - catch(ex) { - // do nothing - this._logger.push({ ex }).log('Error parsing error message body as JSON'); + } + + if (bulkQuote && this._rejectTransfersOnExpiredQuotes) { + const now = new Date(); + const expiration = new Date(bulkQuote.mojaloopResponse.expiration); + if (now > expiration) { + // TODO: Verify and align with actual schema for bulk transfers error endpoint + const error = Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.QUOTE_EXPIRED); + this._logger.error(`Error in prepareBulkTransfers: bulk quotes expired for bulk transfers ${bulkPrepareRequest.bulkTransferId}, system time=${now.toISOString()} > quote time=${expiration.toISOString()}`); + return this._mojaloopRequests.putBulkTransfersError(bulkPrepareRequest.bulkTransferId, error, sourceFspId); + } + } + + if (individualTransferErrors.length) { + // TODO: Verify and align with actual schema for bulk transfers error endpoint + const mojaloopErrorResponse = { + bulkTransferState: 'REJECTED', + // eslint-disable-next-line no-unused-vars + individualTransferResults: individualTransferErrors.map(({ transferId, transferError }) => ({ + transferId, + errorInformation: transferError, + })) + }; + this._logger.push({ ...individualTransferErrors }).log('Error in prepareBulkTransfers'); + this._logger.push({ ...individualTransferErrors }).log(`Sending error response to ${sourceFspId}`); + + return await this._mojaloopRequests.putBulkTransfersError(bulkPrepareRequest.transferId, + mojaloopErrorResponse, sourceFspId); + } + + // project the incoming bulk transfer prepare into an internal bulk transfer request + const internalForm = shared.mojaloopBulkPrepareToInternalBulkTransfer(bulkPrepareRequest, bulkQuote, this._ilp); + + // make a call to the backend to inform it of the incoming bulk transfer + const response = await this._backendRequests.postBulkTransfers(internalForm); + + if (!response) { + // make an error callback to the source fsp + return 'No response from backend'; + } + + this._logger.log(`Bulk transfer accepted by backend returning homeTransactionId: ${response.homeTransactionId} for mojaloop bulk transferId: ${bulkPrepareRequest.bulkTransferId}`); + + // create a mojaloop transfer fulfil response + const mojaloopResponse = { + completedTimestamp: new Date(), + bulkTransferState: 'COMMITTED', + }; + + if (response.individualTransferResults && response.individualTransferResults.length) { + // eslint-disable-next-line no-unused-vars + mojaloopResponse.individualTransferResults = response.individualTransferResults.map((transfer) => { + return { + transferId: transfer.transferId, + fulfilment: fulfilments[transfer.transferId], + ...transfer.extensionList && { + extensionList: { + extension: transfer.extensionList, + }, + } + }; + }); + } + + // make a callback to the source fsp with the transfer fulfilment + return this._mojaloopRequests.putBulkTransfers(bulkPrepareRequest.bulkTransferId, mojaloopResponse, sourceFspId); + } + catch (err) { + this._logger.push({ err }).log('Error in prepareBulkTransfers'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return await this._mojaloopRequests.putBulkTransfersError(bulkPrepareRequest.bulkTransferId, + mojaloopError, sourceFspId); + } + } + + /** + * Queries details of a bulk transfer + */ + async getBulkTransfer(bulkTransferId, sourceFspId) { + try { + // make a call to the backend to get bulk transfer details + const response = await this._backendRequests.getBulkTransfers(bulkTransferId); + + if (!response) { + return 'No response from backend'; + } + + let individualTransferResults = []; + + for (const transfer of response.internalRequest.individualTransfers) { + const ilpPaymentData = { + transferId: transfer.transferId, + to: shared.internalPartyToMojaloopParty(transfer.to, transfer.to.fspId), + amountType: transfer.amountType, + currency: transfer.currency, + amount: transfer.amount, + transactionType: transfer.transactionType, + note: transfer.note, + }; + let fulfilment; + if (this._dfspId === transfer.to.fspId) { + fulfilment = this._ilp.getResponseIlp(ilpPaymentData).fulfilment; } + const transferResult = { transferId: transfer.transferId, fulfilment }; + transfer.errorInformation && (transferResult.errorInformation = transfer.errorInformation); + transfer.extensionList && (transferResult.extensionList = transfer.extensionList); + individualTransferResults.push(transferResult); } + // create a mojaloop bulk transfer fulfil response + const mojaloopResponse = { + completedTimestamp: response.timestamp, + bulkTransferState: response.bulkTransferState, + individualTransferResults, + ...response.extensions && { + extensionList: { + extension: response.extensions, + }, + }, + }; + + // make a callback to the source fsp with the bulk transfer fulfilments + return this._mojaloopRequests.putBulkTransfers(bulkTransferId, mojaloopResponse, + sourceFspId); + } + catch (err) { + this._logger.push({ err }).log('Error in getBulkTransfer'); + const mojaloopError = await this._handleError(err); + this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`); + return this._mojaloopRequests.putBulkTransfersError(bulkTransferId, + mojaloopError, sourceFspId); + } + } + + /** + * Forwards Switch notification for fulfiled transfer to the DFSP backend, when acting as a payee + */ + async sendNotificationToPayee(body, transferId) { + try { + const res = await this._backendRequests.putTransfersNotification(body, transferId); + return res; + } catch (err) { + this._logger.push({ err }).log('Error in sendNotificationToPayee'); + } + } + + async _handleError(err, mojaloopErrorCode = Errors.MojaloopApiErrorCodes.INTERNAL_SERVER_ERROR) { + if(err instanceof HTTPResponseError) { + const e = err.getData(); + if(e.res && e.res.data) { + mojaloopErrorCode = Errors.MojaloopApiErrorCodeFromCode(`${e.res.data.statusCode}`); + } } return new Errors.MojaloopFSPIOPError(err, null, null, mojaloopErrorCode).toApiErrorObject(); diff --git a/src/lib/model/OutboundAuthorizationsModel.js b/src/lib/model/OutboundAuthorizationsModel.js new file mode 100644 index 000000000..762761a87 --- /dev/null +++ b/src/lib/model/OutboundAuthorizationsModel.js @@ -0,0 +1,253 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; + +const util = require('util'); +const { uuid } = require('uuidv4'); +const PSM = require('./common').PersistentStateMachine; +const ThirdpartyRequests = require('@mojaloop/sdk-standard-components').ThirdpartyRequests; + + +const specStateMachine = { + init: 'start', + transitions: [ + { name: 'init', from: 'none', to: 'start' }, + { name: 'requestAuthorization', from: 'start', to: 'succeeded' }, + { name: 'error', from: '*', to: 'errored' }, + ], + methods: { + // workflow methods + run, + getResponse, + + // specific transitions handlers methods + onRequestAuthorization, + } +}; + +/** + * runs the workflow + */ +async function run() { + const { data, logger } = this.context; + try { + // run transitions based on incoming state + switch(data.currentState) { + case 'start': + // the first transition is requestAuthorization + await this.requestAuthorization(); + logger.log(`Authorization requested for ${data.transactionRequestId}, currentState: ${data.currentState}`); + + // eslint-disable-next-line no-fallthrough + case 'succeeded': + // all steps complete so return + logger.log('Authorization completed successfully'); + return this.getResponse(); + + case 'errored': + // stopped in errored state + logger.log('State machine in errored state'); + return; + } + + } catch (err) { + logger.log(`Error running authorizations model: ${util.inspect(err)}`); + + // as this function is recursive, we don't want to error the state machine multiple times + if(data.currentState !== 'errored') { + // err should not have a authorizationState property here! + if(err.authorizationState) { + logger.log('State machine is broken'); + } + // transition to errored state + await this.error(err); + + // avoid circular ref between authorizationState.lastError and err + err.authorizationState = JSON.parse(JSON.stringify(this.getResponse())); + } + throw err; + } +} + + +const mapCurrentState = { + start: 'WAITING_FOR_AUTHORIZATION_REQUEST', + succeeded: 'COMPLETED', + errored: 'ERROR_OCCURRED' +}; + + +/** + * Returns an object representing the final state of the authorization suitable for the outbound API + * + * @returns {object} - Response representing the result of the authorization process + */ +function getResponse() { + const { data, logger } = this.context; + let resp = { ...data }; + + // project some of our internal state into a more useful + // representation to return to the SDK API consumer + resp.currentState = mapCurrentState[data.currentState]; + + // handle unexpected state + if(!resp.currentState) { + logger.log(`Authorization model response being returned from an unexpected state: ${data.currentState}. Returning ERROR_OCCURRED state`); + resp.currentState = mapCurrentState.errored; + } + + return resp; +} + +function notificationChannel(id) { + // mvp validation + if(!(id && id.toString().length > 0)) { + throw new Error('OutboundAuthorizationsModel.notificationChannel: \'id\' parameter is required'); + } + + // channel name + return `authorizations_${id}`; +} + +/** + * Requests Authorization + * Starts the authorization process by sending a POST /authorizations request to switch; + * than await for a notification on PUT /authorizations/ from the cache that the Authorization has been resolved + */ +async function onRequestAuthorization() { + const { data, cache, logger } = this.context; + const { requests, config } = this.handlersContext; + const channel = notificationChannel(data.transactionRequestId); + let subId; + + // eslint-disable-next-line no-async-promise-executor + return new Promise( async(resolve, reject) => { + + try { + // in InboundServer/handlers is implemented putAuthorizationsById handler + // where this event is fired but only if env ENABLE_PISP_MODE=true + subId = await cache.subscribe(channel, async (channel, message, sid) => { + try { + const parsed = JSON.parse(message); + this.context.data = { + ...parsed.data, + currentState: this.state + }; + resolve(); + } catch(err) { + reject(err); + } finally { + if(sid) { + cache.unsubscribe(channel, sid); + } + } + }); + + // POST /authorization request to the switch + const postRequest = buildPostAuthorizationsRequest(data, config); + const res = await requests.postAuthorizations(postRequest, data.toParticipantId); + + logger.push({ res }).log('Authorizations request sent to peer'); + + } catch(error) { + logger.push(error).error('Authorization request error'); + if(subId) { + cache.unsubscribe(channel, subId); + } + reject(error); + } + }); +} + +function buildPostAuthorizationsRequest(data/** , config */) { + // TODO: the request object must be valid to schema defined in sdk-standard-components + const request = { + ...data + }; + + // drop properties not conforming to the txr service schema + delete request.toParticipantId; + delete request.currentState; + + return request; +} + +/** + * injects the config into state machine data + * so it will be accessible to on transition notification handlers via `this.handlersContext` + * + * @param {Object} config - config to be injected into state machine data + * @param {Object} specStateMachine - specState machine to be altered + * @returns {Object} - the altered specStateMachine + */ +function injectHandlersContext(config, specStateMachine) { + return { + ...specStateMachine, + data: { + handlersContext: { + config, // injects config property + requests: new ThirdpartyRequests({ + logger: config.logger, + peerEndpoint: config.peerEndpoint, + alsEndpoint: config.alsEndpoint, + quotesEndpoint: config.quotesEndpoint, + transfersEndpoint: config.transfersEndpoint, + transactionRequestsEndpoint: config.transactionRequestsEndpoint, + dfspId: config.dfspId, + tls: config.outbound.tls, + jwsSign: config.jwsSign, + jwsSignPutParties: config.jwsSignPutParties, + jwsSigningKey: config.jwsSigningKey, + wso2: config.wso2, + }) + } + } + }; +} + + +/** + * creates a new instance of state machine specified in specStateMachine ^ + * + * @param {Object} data - payload data + * @param {String} key - the cache key where state machine will store the payload data after each transition + * @param {Object} config - the additional configuration for transition handlers + */ +async function create(data, key, config) { + + if(!data.hasOwnProperty('transactionRequestId')) { + data.transactionRequestId = uuid(); + } + + const spec = injectHandlersContext(config, specStateMachine); + return PSM.create(data, config.cache, key, config.logger, spec); +} + + +/** + * loads state machine from cache by given key and specify the additional config for transition handlers + * @param {String} key - the cache key used to retrieve the state machine from cache + * @param {Object} config - the additional configuration for transition handlers + */ +async function loadFromCache(key, config) { + const customCreate = async (data, cache, key /**, logger, stateMachineSpec **/) => create(data, key, config); + return PSM.loadFromCache(config.cache, key, config.logger, specStateMachine, customCreate); +} + +module.exports = { + create, + loadFromCache, + notificationChannel, + + // exports for testing purposes + mapCurrentState, + buildPostAuthorizationsRequest +}; diff --git a/src/lib/model/OutboundBulkQuotesModel.js b/src/lib/model/OutboundBulkQuotesModel.js new file mode 100644 index 000000000..cc4fbd195 --- /dev/null +++ b/src/lib/model/OutboundBulkQuotesModel.js @@ -0,0 +1,485 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Steven Oderayi - steven.oderayi@modusbox.com * + **************************************************************************/ + +'use strict'; + +const util = require('util'); +const { uuid } = require('uuidv4'); +const StateMachine = require('javascript-state-machine'); +const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const shared = require('@internal/shared'); +const { BackendError } = require('./common'); + +const stateEnum = { + 'ERROR_OCCURRED': 'ERROR_OCCURRED', + 'COMPLETED': 'COMPLETED', +}; + + +/** + * Models the state machine and operations required for performing an outbound bulk quote request + */ +class OutboundBulkQuotesModel { + constructor(config) { + this._cache = config.cache; + this._logger = config.logger; + this._requestProcessingTimeoutSeconds = config.requestProcessingTimeoutSeconds; + this._dfspId = config.dfspId; + this._expirySeconds = config.expirySeconds; + this._rejectExpiredQuoteResponses = config.rejectExpiredQuoteResponses; + + this._requests = new MojaloopRequests({ + logger: this._logger, + peerEndpoint: config.peerEndpoint, + bulkQuotesEndpoint: config.bulkQuotesEndpoint, + dfspId: config.dfspId, + tls: config.outbound.tls, + jwsSign: config.jwsSign, + jwsSigningKey: config.jwsSigningKey, + wso2: config.wso2, + }); + } + + /** + * Initializes the internal state machine object + */ + _initStateMachine (initState) { + this.stateMachine = new StateMachine({ + init: initState, + transitions: [ + { name: 'requestBulkQuote', from: 'start', to: 'succeeded' }, + { name: 'getBulkQuote', to: 'succeeded' }, + { name: 'error', from: '*', to: 'errored' }, + ], + methods: { + onTransition: this._handleTransition.bind(this), + onAfterTransition: this._afterTransition.bind(this), + onPendingTransition: (transition, from, to) => { + // allow transitions to 'error' state while other transitions are in progress + if(transition !== 'error') { + throw new Error(`Transition requested while another transition is in progress: ${transition} from: ${from} to: ${to}`); + } + } + } + }); + + return this.stateMachine[initState]; + } + + /** + * Updates the internal state representation to reflect that of the state machine itself + */ + _afterTransition() { + this._logger.log(`State machine transitioned: ${this.data.currentState} -> ${this.stateMachine.state}`); + this.data.currentState = this.stateMachine.state; + } + + /** + * Initializes the bulk quotes model + * + * @param data {object} - The inbound API POST /bulkQuotes request body + */ + async initialize(data) { + this.data = data; + + // add a bulkQuoteId if one is not present e.g. on first submission + if(!this.data.hasOwnProperty('bulkQuoteId')) { + this.data.bulkQuoteId = uuid(); + } + + // initialize the state machine to its starting state + if(!this.data.hasOwnProperty('currentState')) { + this.data.currentState = 'start'; + } + + this._initStateMachine(this.data.currentState); + } + + /** + * Handles state machine transitions + */ + async _handleTransition(lifecycle, ...args) { + this._logger.log(`Bulk quote ${this.data.bulkQuoteId} is transitioning from ${lifecycle.from} to ${lifecycle.to} in response to ${lifecycle.transition}`); + + switch(lifecycle.transition) { + case 'init': + return; + + case 'requestBulkQuote': + return this._requestBulkQuote(); + + case 'getBulkQuote': + return this._getBulkQuote(this.data.bulkQuoteId); + + case 'error': + this._logger.log(`State machine is erroring with error: ${util.inspect(args)}`); + this.data.lastError = args[0] || new Error('unspecified error'); + break; + + default: + throw new Error(`Unhandled state transition for bulk quote ${this.data.bulkQuoteId}: ${util.inspect(args)}`); + } + } + + /** + * Requests a bulk quote + * Starts the quotes resolution process by sending a POST /bulkQuotes request to the switch; + * then waits for a notification from the cache that the quotes response has been received. + */ + async _requestBulkQuote() { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + // create a bulk quote request + const bulkQuote = this._buildBulkQuoteRequest(); + + // listen for events on the bulkQuoteId + const bulkQuoteKey = `bulkQuote_${bulkQuote.bulkQuoteId}`; + + // hook up a subscriber to handle response messages + const subId = await this._cache.subscribe(bulkQuoteKey, (cn, msg, subId) => { + try { + let error; + let message = JSON.parse(msg); + + if (message.type === 'bulkQuoteResponse') { + if (this._rejectExpiredQuoteResponses) { + const now = new Date().toISOString(); + if (now > bulkQuote.expiration) { + const msg = 'Bulk quote response missed expiry deadline'; + error = new BackendError(msg, 504); + this._logger.error(`${msg}: system time=${now} > expiration time=${bulkQuote.expiration}`); + } + } + } else if (message.type === 'bulkQuoteResponseError') { + error = new BackendError(`Got an error response requesting bulk quote: ${util.inspect(message.data, { depth: Infinity })}`, 500); + error.mojaloopError = message.data; + } + else { + this._logger.push({ message }).log(`Ignoring cache notification for bulk quote ${bulkQuoteKey}. Unknown message type ${message.type}.`); + return; + } + + // cancel the timeout handler + clearTimeout(timeout); + + // stop listening for bulk quote resolution messages + // no need to await for the unsubscribe to complete. + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkQuoteKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in callback) ${bulkQuoteKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + if (error) { + return reject(error); + } + + const bulkQuoteResponseBody = message.data; + this._logger.push({ bulkQuoteResponseBody }).log('Bulk quote response received'); + + return resolve(bulkQuoteResponseBody); + } + catch (err) { + return reject(err); + } + }); + + // set up a timeout for the request + const timeout = setTimeout(() => { + const err = new BackendError(`Timeout requesting bulk quote ${this.data.bulkQuoteId}`, 504); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkQuoteKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in timeout handler) ${bulkQuoteKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + }, this._requestProcessingTimeoutSeconds * 1000); + + // now we have a timeout handler and a cache subscriber hooked up we can fire off + // a POST /bulkQuotes request to the switch + try { + const res = await this._requests.postBulkQuotes(bulkQuote, this.data.individualQuotes[0].to.fspId); + this._logger.push({ res }).log('Bulk quote request sent to peer'); + } + catch (err) { + // cancel the timout and unsubscribe before rejecting the promise + clearTimeout(timeout); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkQuoteKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in error handler) ${bulkQuoteKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + } + }); + } + + /** + * Constructs a bulk quote request payload based on current state + * + * @returns {object} - the bulk quote request object + */ + _buildBulkQuoteRequest() { + const bulkQuoteRequest = { + bulkQuoteId: this.data.bulkQuoteId, + payer: shared.internalPartyToMojaloopParty(this.data.from, this._dfspId), + expiration: this._getExpirationTimestamp(), + }; + + this.data.geoCode && (bulkQuoteRequest.geoCode = this.data.geoCode); + + if (this.data.extensions && this.data.extensions.length > 0) { + bulkQuoteRequest.extensionList = { + extension: this.data.extensions + }; + } + + bulkQuoteRequest.individualQuotes = this.data.individualQuotes.map((individualQuote) => { + const quoteId = individualQuote.quoteId || uuid(); + const quote = { + quoteId: quoteId, + transactionId: individualQuote.transactionId || quoteId, + payee: shared.internalPartyToMojaloopParty(individualQuote.to, individualQuote.to.fspId), + amountType: individualQuote.amountType, + amount: { + currency: individualQuote.currency, + amount: individualQuote.amount + }, + transactionType: { + scenario: individualQuote.transactionType, + // TODO: support payee initiated txns? + initiator: 'PAYER', + // TODO: defaulting to CONSUMER initiator type should + // be replaced with a required element on the incoming + // API request + initiatorType: this.data.from.type || 'CONSUMER' + } + }; + + individualQuote.note && (quote.note = individualQuote.note); + + if (individualQuote.extensions && individualQuote.extensions.length > 0) { + bulkQuoteRequest.extensionList = { + extension: individualQuote.extensions + }; + } + + return quote; + }); + + return bulkQuoteRequest; + } + + /** + * Get bulk quote details by sending GET /bulkQuotes/{ID} request to the switch + */ + async _getBulkQuote(bulkQuoteId) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + const bulkQuoteKey = `bulkQuote_${bulkQuoteId}`; + + // hook up a subscriber to handle response messages + const subId = await this._cache.subscribe(bulkQuoteKey, (cn, msg, subId) => { + try { + let error; + let message = JSON.parse(msg); + + if (message.type === 'bulkQuoteError') { + error = new BackendError(`Got an error response retrieving bulk quote: ${util.inspect(message.data, { depth: Infinity })}`, 500); + error.mojaloopError = message.data; + } else if (message.type !== 'bulkQuoteResponse') { + this._logger.push({ message }).log(`Ignoring cache notification for bulk quote ${bulkQuoteKey}. Uknokwn message type ${message.type}.`); + return; + } + + // cancel the timeout handler + clearTimeout(timeout); + + // stop listening for bulk quote response messages + this._cache.unsubscribe(bulkQuoteKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in callback) ${bulkQuoteKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + if (error) { + return reject(error); + } + + const bulkQuote = message.data; + this._logger.push({ bulkQuote }).log('Bulk quote response received'); + + return resolve(bulkQuote); + } + catch(err) { + return reject(err); + } + }); + + // set up a timeout for the resolution + const timeout = setTimeout(() => { + const err = new BackendError(`Timeout getting bulk quote ${bulkQuoteId}`, 504); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkQuoteKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in timeout handler) ${bulkQuoteKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + }, this._requestProcessingTimeoutSeconds * 1000); + + // now we have a timeout handler and a cache subscriber hooked up we can fire off + // a GET /bulkQuotes/{ID} request to the switch + try { + const res = await this._requests.getBulkQuotes(bulkQuoteId); + this._logger.push({ peer: res }).log('Bulk quote lookup sent to peer'); + } + catch(err) { + // cancel the timout and unsubscribe before rejecting the promise + clearTimeout(timeout); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkQuoteKey, subId).catch(e => { + this._logger.log(`Error unsubscribing ${bulkQuoteKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + } + }); + } + + /** + * Returns an ISO-8601 format timestamp n-seconds in the future for expiration of a bulk quote API object, + * where n is equal to our config setting "expirySeconds" + * + * @returns {string} - ISO-8601 format future expiration timestamp + */ + _getExpirationTimestamp() { + let now = new Date(); + return new Date(now.getTime() + (this._expirySeconds * 1000)).toISOString(); + } + + /** + * Returns an object representing the final state of the bulk quote suitable for the outbound API + * + * @returns {object} - Response representing the result of the bulk quoting process + */ + getResponse() { + // we want to project some of our internal state into a more useful + // representation to return to the SDK API consumer + let resp = { ...this.data }; + + switch(this.data.currentState) { + case 'succeeded': + resp.currentState = stateEnum.COMPLETED; + break; + + case 'errored': + resp.currentState = stateEnum.ERROR_OCCURRED; + break; + + default: + this._logger.log(`Bulk quote model response being returned from an unexpected state: ${this.data.currentState}. Returning ERROR_OCCURRED state`); + resp.currentState = stateEnum.ERROR_OCCURRED; + break; + } + + return resp; + } + + /** + * Persists the model state to cache for reinstantiation at a later point + */ + async _save() { + try { + this.data.currentState = this.stateMachine.state; + const res = await this._cache.set(`bulkQuoteModel_${this.data.bulkQuoteId}`, this.data); + this._logger.push({ res }).log('Persisted bulk quote model in cache'); + } + catch(err) { + this._logger.push({ err }).log('Error saving bulk quote model'); + throw err; + } + } + + /** + * Loads a bulk quote model from cache for resumption of the bulk quote process + * + * @param bulkQuoteId {string} - UUID bulkQuoteId of the model to load from cache + */ + async load(bulkQuoteId) { + try { + const data = await this._cache.get(`bulkQuoteModel_${bulkQuoteId}`); + if(!data) { + throw new Error(`No cached data found for bulkQuoteId: ${bulkQuoteId}`); + } + await this.initialize(data); + this._logger.push({ cache: this.data }).log('Bulk quote model loaded from cached state'); + } + catch(err) { + this._logger.push({ err }).log('Error loading bulk quote model'); + throw err; + } + } + + /** + * Returns a promise that resolves when the state machine has reached a terminal state + */ + async run() { + try { + // run transitions based on incoming state + switch(this.data.currentState) { + case 'start': + await this.stateMachine.requestBulkQuote(); + this._logger.log(`Quotes resolved for bulk quote ${this.data.bulkQuoteId}`); + break; + + case 'getBulkQuote': + await this.stateMachine.getBulkQuote(); + this._logger.log(`Get bulk quote ${this.data.bulkQuoteId} has been completed`); + break; + + case 'succeeded': + // all steps complete so return + this._logger.log('Bulk quoting completed successfully'); + await this._save(); + return this.getResponse(); + + case 'errored': + // stopped in errored state + this._logger.log('State machine in errored state'); + return; + } + + // now call ourselves recursively to deal with the next transition + this._logger.log(`Bulk quote model state machine transition completed in state: ${this.stateMachine.state}. Recursing to handle next transition.`); + return this.run(); + } + catch(err) { + this._logger.log(`Error running bulk quote model: ${util.inspect(err)}`); + + // as this function is recursive, we dont want to error the state machine multiple times + if(this.data.currentState !== 'errored') { + // err should not have a bulkQuoteState property here! + if(err.bulkQuoteState) { + this._logger.log(`State machine is broken: ${util.inspect(err)}`); + } + // transition to errored state + await this.stateMachine.error(err); + + // avoid circular ref between bulkQuoteState.lastError and err + err.bulkQuoteState = JSON.parse(JSON.stringify(this.getResponse())); + } + throw err; + } + } +} + + +module.exports = OutboundBulkQuotesModel; diff --git a/src/lib/model/OutboundBulkTransfersModel.js b/src/lib/model/OutboundBulkTransfersModel.js new file mode 100644 index 000000000..fb0bbed28 --- /dev/null +++ b/src/lib/model/OutboundBulkTransfersModel.js @@ -0,0 +1,479 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Steven Oderayi - steven.oderayi@modusbox.com * + **************************************************************************/ + +'use strict'; + +const util = require('util'); +const { uuid } = require('uuidv4'); +const StateMachine = require('javascript-state-machine'); +const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const { BackendError } = require('./common'); + +const stateEnum = { + 'ERROR_OCCURRED': 'ERROR_OCCURRED', + 'COMPLETED': 'COMPLETED', +}; + +/** + * Models the state machine and operations required for performing an outbound bulk transfer + */ +class OutboundBulkTransfersModel { + constructor(config) { + this._cache = config.cache; + this._logger = config.logger; + this._requestProcessingTimeoutSeconds = config.requestProcessingTimeoutSeconds; + this._dfspId = config.dfspId; + this._expirySeconds = config.expirySeconds; + this._rejectExpiredTransferFulfils = config.rejectExpiredTransferFulfils; + + this._requests = new MojaloopRequests({ + logger: this._logger, + peerEndpoint: config.peerEndpoint, + bulkTransfersEndpoint: config.bulkTransfersEndpoint, + dfspId: config.dfspId, + tls: config.outbound.tls, + jwsSign: config.jwsSign, + jwsSignPutParties: config.jwsSignPutParties, + jwsSigningKey: config.jwsSigningKey, + wso2: config.wso2, + }); + } + + /** + * Initializes the internal state machine object + */ + _initStateMachine (initState) { + this.stateMachine = new StateMachine({ + init: initState, + transitions: [ + { name: 'executeBulkTransfer', from: 'start', to: 'succeeded' }, + { name: 'getBulkTransfer', to: 'succeeded' }, + { name: 'error', from: '*', to: 'errored' }, + ], + methods: { + onTransition: this._handleTransition.bind(this), + onAfterTransition: this._afterTransition.bind(this), + onPendingTransition: (transition, from, to) => { + // allow transitions to 'error' state while other transitions are in progress + if(transition !== 'error') { + throw new Error(`Transition requested while another transition is in progress: ${transition} from: ${from} to: ${to}`); + } + } + } + }); + + return this.stateMachine[initState]; + } + + /** + * Updates the internal state representation to reflect that of the state machine itself + */ + _afterTransition() { + this._logger.log(`State machine transitioned: ${this.data.currentState} -> ${this.stateMachine.state}`); + this.data.currentState = this.stateMachine.state; + } + + /** + * Initializes the bulk transfer model + * + * @param data {object} - The inbound API POST /bulkTransfers request body + */ + async initialize(data) { + this.data = data; + + // add a bulkTransferId if one is not present e.g. on first submission + if(!this.data.hasOwnProperty('bulkTransferId')) { + this.data.bulkTransferId = uuid(); + } + + // initialize the bulk transfer state machine to its starting state + if(!this.data.hasOwnProperty('currentState')) { + this.data.currentState = 'start'; + } + + this._initStateMachine(this.data.currentState); + } + + /** + * Handles state machine transitions + */ + async _handleTransition(lifecycle, ...args) { + this._logger.log(`Bulk transfer ${this.data.bulkTransferId} is transitioning from ${lifecycle.from} to ${lifecycle.to} in response to ${lifecycle.transition}`); + + switch(lifecycle.transition) { + case 'init': + return; + + case 'executeBulkTransfer': + return this._executeBulkTransfer(); + + case 'getBulkTransfer': + return this._getBulkTransfer(this.data.bulkTransferId); + + case 'error': + this._logger.log(`State machine is erroring with error: ${util.inspect(args)}`); + this.data.lastError = args[0] || new Error('unspecified error'); + break; + + default: + throw new Error(`Unhandled state transition for bulk transfer ${this.data.bulkTransferId}: ${util.inspect(args)}`); + } + } + + /** + * Executes a bulk transfer + * Starts the transfer process by sending a POST /bulkTransfers (prepare) request to the switch; + * then waits for a notification from the cache that the transfer has been fulfilled + */ + async _executeBulkTransfer() { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + // create a bulk transfer request + const bulkTransferPrepare = this._buildBulkTransferPrepareRequest(); + + // listen for events on the bulkTransferId + const bulkTransferKey = `bulkTransfer_${this.data.bulkTransferId}`; + + // hook up a subscriber to handle response messages + const subId = await this._cache.subscribe(bulkTransferKey, (cn, msg, subId) => { + try { + let error; + let message = JSON.parse(msg); + + if (message.type === 'bulkTransferFulfil') { + if (this._rejectExpiredTransferFulfils) { + const now = new Date().toISOString(); + if (now > bulkTransferPrepare.expiration) { + const msg = 'Bulk transfer fulfils missed expiry deadline'; + error = new BackendError(msg, 504); + this._logger.error(`${msg}: system time=${now} > expiration time=${bulkTransferPrepare.expiration}`); + } + } + } else if (message.type === 'bulkTransferError') { + error = new BackendError(`Got an error response preparing bulk transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500); + error.mojaloopError = message.data; + } + else { + this._logger.push({ message }).log(`Ignoring cache notification for bulk transfer ${bulkTransferKey}. Unknown message type ${message.type}.`); + return; + } + + // cancel the timeout handler + clearTimeout(timeout); + + // stop listening for bulk transfer resolution messages + // no need to await for the unsubscribe to complete. + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkTransferKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in callback) ${bulkTransferKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + if (error) { + return reject(error); + } + + const bulkTransferFulfil = message.data; + this._logger.push({ bulkTransferFulfil }).log('Bulk transfer fulfils received'); + + return resolve(bulkTransferFulfil); + } + catch (err) { + return reject(err); + } + }); + + // set up a timeout for the request + const timeout = setTimeout(() => { + const err = new BackendError(`Timeout waiting for fulfil for bulk transfer ${this.data.bulkTransferId}`, 504); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkTransferKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in timeout handler) ${bulkTransferKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + }, this._requestProcessingTimeoutSeconds * 1000); + + // now we have a timeout handler and a cache subscriber hooked up we can fire off + // a POST /bulkTransfers request to the switch + try { + const res = await this._requests.postBulkTransfers(bulkTransferPrepare, this.data.from.fspId); + this._logger.push({ res }).log('Bulk transfer request sent to peer'); + } + catch (err) { + // cancel the timout and unsubscribe before rejecting the promise + clearTimeout(timeout); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkTransferKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in error handler) ${bulkTransferKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + } + }); + } + + /** + * Constructs a bulk transfer request payload + * + * @returns {object} - the bulk transfer request object + */ + _buildBulkTransferPrepareRequest() { + const bulkTransferRequest = { + bulkTransferId: this.data.bulkTransferId, + bulkQuoteId: this.data.bulkQuoteId, + payerFsp: this._dfspId, + payeeFsp: this.data.individualTransfers[0].to.fspId, + expiration: this._getExpirationTimestamp() + }; + + // add extensionList if provided + if (this.data.extensions && this.data.extensions.length > 0) { + bulkTransferRequest.extensionList = { + extension: this.data.extensions + }; + } + + bulkTransferRequest.individualTransfers = this.data.individualTransfers.map((individualTransfer) => { + if (bulkTransferRequest.payeeFsp !== individualTransfer.to.fspId) throw new BackendError('payee fsps are not the same into the whole bulk', 500); + + const transferId = individualTransfer.transferId || uuid(); + + const transferPrepare = { + transferId: transferId, + transferAmount: { + currency: individualTransfer.currency, + amount: individualTransfer.amount + }, + ilpPacket: individualTransfer.ilpPacket, + condition: individualTransfer.condition, + }; + + if (individualTransfer.extensions && individualTransfer.extensions.length > 0) { + bulkTransferRequest.extensionList = { + extension: individualTransfer.extensions + }; + } + + + return transferPrepare; + }); + + return bulkTransferRequest; + } + + /** + * Get bulk transfer details by sending GET /bulkTransfers/{ID} request to the switch + */ + async _getBulkTransfer(bulkTransferId) { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + const bulkTransferKey = `bulkTransfer_${bulkTransferId}`; + + // hook up a subscriber to handle response messages + const subId = await this._cache.subscribe(bulkTransferKey, (cn, msg, subId) => { + try { + let error; + let message = JSON.parse(msg); + + if (message.type === 'bulkTransferError') { + error = new BackendError(`Got an error response retrieving bulk transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500); + error.mojaloopError = message.data; + } else if (message.type !== 'bulkTransferFulfil') { + this._logger.push({ message }).log(`Ignoring cache notification for bulk transfer ${bulkTransferKey}. Uknokwn message type ${message.type}.`); + return; + } + + // cancel the timeout handler + clearTimeout(timeout); + + // stop listening for bulk transfer fulfil messages + this._cache.unsubscribe(bulkTransferKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in callback) ${bulkTransferKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + if (error) { + return reject(error); + } + + const fulfils = message.data; + this._logger.push({ fulfils }).log('Bulk transfer fulfils received'); + + return resolve(fulfils); + } + catch(err) { + return reject(err); + } + }); + + // set up a timeout for the resolution + const timeout = setTimeout(() => { + const err = new BackendError(`Timeout getting bulk transfer ${bulkTransferId}`, 504); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkTransferKey, subId).catch(e => { + this._logger.log(`Error unsubscribing (in timeout handler) ${bulkTransferKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + }, this._requestProcessingTimeoutSeconds * 1000); + + // now we have a timeout handler and a cache subscriber hooked up we can fire off + // a GET /bulkTransfers/{ID} request to the switch + try { + const res = await this._requests.getBulkTransfers(bulkTransferId); + this._logger.push({ peer: res }).log('Bulk transfer lookup sent to peer'); + } + catch(err) { + // cancel the timout and unsubscribe before rejecting the promise + clearTimeout(timeout); + + // we dont really care if the unsubscribe fails but we should log it regardless + this._cache.unsubscribe(bulkTransferKey, subId).catch(e => { + this._logger.log(`Error unsubscribing ${bulkTransferKey} ${subId}: ${e.stack || util.inspect(e)}`); + }); + + return reject(err); + } + }); + } + + /** + * Returns an ISO-8601 format timestamp n-seconds in the future for expiration of a bulk quote API object, + * where n is equal to our config setting "expirySeconds" + * + * @returns {string} - ISO-8601 format future expiration timestamp + */ + _getExpirationTimestamp() { + let now = new Date(); + return new Date(now.getTime() + (this._expirySeconds * 1000)).toISOString(); + } + + /** + * Returns an object representing the final state of the bulk transfer suitable for the outbound API + * + * @returns {object} - Response representing the result of the bulk transfer process + */ + getResponse() { + // we want to project some of our internal state into a more useful + // representation to return to the SDK API consumer + let resp = { ...this.data }; + + switch(this.data.currentState) { + case 'succeeded': + resp.currentState = stateEnum.COMPLETED; + break; + + case 'errored': + resp.currentState = stateEnum.ERROR_OCCURRED; + break; + + default: + this._logger.log(`Bulk transfer model response being returned from an unexpected state: ${this.data.currentState}. Returning ERROR_OCCURRED state`); + resp.currentState = stateEnum.ERROR_OCCURRED; + break; + } + + return resp; + } + + /** + * Persists the model state to cache for reinstantiation at a later point + */ + async _save() { + try { + this.data.currentState = this.stateMachine.state; + const res = await this._cache.set(`bulkTransferModel_${this.data.bulkTransferId}`, this.data); + this._logger.push({ res }).log('Persisted bulk transfer model in cache'); + } + catch(err) { + this._logger.push({ err }).log('Error saving bulk transfer model'); + throw err; + } + } + + + /** + * Loads a bulk transfer model from cache for resumption of the bulk transfer process + * + * @param bulkTransferId {string} - UUID bulkTransferId of the model to load from cache + */ + async load(bulkTransferId) { + try { + const data = await this._cache.get(`bulkTransferModel_${bulkTransferId}`); + if(!data) { + throw new Error(`No cached data found for bulkTransferId: ${bulkTransferId}`); + } + await this.initialize(data); + this._logger.push({ cache: this.data }).log('Bulk transfer model loaded from cached state'); + } + catch(err) { + this._logger.push({ err }).log('Error loading bulk transfer model'); + throw err; + } + } + + /** + * Returns a promise that resolves when the state machine has reached a terminal state + */ + async run() { + try { + // run transitions based on incoming state + switch(this.data.currentState) { + case 'start': + await this.stateMachine.executeBulkTransfer(); + this._logger.log(`Bulk transfer ${this.data.bulkTransferId} has been completed`); + break; + + case 'getBulkTransfer': + await this.stateMachine.getBulkTransfer(); + this._logger.log(`Get bulk transfer ${this.data.bulkTransferId} has been completed`); + break; + + case 'succeeded': + // all steps complete so return + this._logger.log('Bulk transfer completed successfully'); + await this._save(); + return this.getResponse(); + + case 'errored': + // stopped in errored state + this._logger.log('State machine in errored state'); + return; + } + + // now call ourselves recursively to deal with the next transition + this._logger.log(`Bulk transfer model state machine transition completed in state: ${this.stateMachine.state}. Recursing to handle next transition.`); + return this.run(); + } + catch(err) { + this._logger.log(`Error running transfer model: ${util.inspect(err)}`); + + // as this function is recursive, we dont want to error the state machine multiple times + if(this.data.currentState !== 'errored') { + // err should not have a bulkTransferState property here! + if(err.bulkTransferState) { + this._logger.log(`State machine is broken: ${util.inspect(err)}`); + } + // transition to errored state + await this.stateMachine.error(err); + + // avoid circular ref between bulkTransferState.lastError and err + err.bulkTransferState = JSON.parse(JSON.stringify(this.getResponse())); + } + throw err; + } + } + +} + + +module.exports = OutboundBulkTransfersModel; diff --git a/src/lib/model/OutboundRequestToPayModel.js b/src/lib/model/OutboundRequestToPayModel.js index a14fd8a57..c22b04597 100644 --- a/src/lib/model/OutboundRequestToPayModel.js +++ b/src/lib/model/OutboundRequestToPayModel.js @@ -32,17 +32,17 @@ class OutboundRequestToPayModel { this._dfspId = config.dfspId; this._expirySeconds = config.expirySeconds; this._autoAcceptParty = config.autoAcceptParty; - + this._requests = new MojaloopRequests({ logger: this._logger, peerEndpoint: config.peerEndpoint, alsEndpoint: config.alsEndpoint, dfspId: config.dfspId, - tls: config.tls, + tls: config.outbound.tls, jwsSign: config.jwsSign, jwsSignPutParties: config.jwsSignPutParties, jwsSigningKey: config.jwsSigningKey, - wso2Auth: config.wso2Auth + wso2: config.wso2, }); } @@ -293,7 +293,7 @@ class OutboundRequestToPayModel { this._logger.push({ transactionRequestResponse }).log('Transaction Request Response received'); this.data.requestToPayState = transactionRequestResponse.transactionRequestState; - + return resolve(transactionRequestResponse); } catch(err) { @@ -510,4 +510,4 @@ class OutboundRequestToPayModel { } } -module.exports = OutboundRequestToPayModel; \ No newline at end of file +module.exports = OutboundRequestToPayModel; diff --git a/src/lib/model/OutboundRequestToPayTransferModel.js b/src/lib/model/OutboundRequestToPayTransferModel.js index e4231963c..a6ffd863f 100644 --- a/src/lib/model/OutboundRequestToPayTransferModel.js +++ b/src/lib/model/OutboundRequestToPayTransferModel.js @@ -16,10 +16,12 @@ const StateMachine = require('javascript-state-machine'); const { Ilp, MojaloopRequests } = require('@mojaloop/sdk-standard-components'); const shared = require('@internal/shared'); const { BackendError } = require('./common'); - +const { BackendRequests } = require('@internal/requests'); +const OutboundAuthorizationsModel = require('./OutboundAuthorizationsModel.js'); const requestToPayTransferStateEnum = { 'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE', 'WAITING_FOR_OTP_ACCEPTANCE': 'WAITING_FOR_OTP_ACCEPTANCE', + 'WAITING_FOR_AUTHORIZATION_ACCEPTANCE': 'WAITING_FOR_AUTHORIZATION_ACCEPTANCE', 'ERROR_OCCURRED': 'ERROR_OCCURRED', 'COMPLETED': 'COMPLETED', }; @@ -30,6 +32,7 @@ const requestToPayTransferStateEnum = { */ class OutboundRequestToPayTransferModel { constructor(config) { + this._config = { ...config }; this._cache = config.cache; this._logger = config.logger; this._requestProcessingTimeoutSeconds = config.requestProcessingTimeoutSeconds; @@ -51,16 +54,26 @@ class OutboundRequestToPayTransferModel { authorizationsEndpoint: config.authorizationsEndpoint, transfersEndpoint: config.transfersEndpoint, dfspId: config.dfspId, - tls: config.tls, + tls: config.outbound.tls, jwsSign: config.jwsSign, jwsSignPutParties: config.jwsSignPutParties, jwsSigningKey: config.jwsSigningKey, - wso2Auth: config.wso2Auth + wso2: config.wso2, + }); + + this._backendRequests = new BackendRequests({ + logger: this._logger, + backendEndpoint: config.backendEndpoint, + dfspId: config.dfspId }); this._ilp = new Ilp({ - secret: config.ilpSecret + secret: config.ilpSecret, + logger: this._logger, }); + + this._enablePISPMode = config.enablePISPMode; + this._logger.info('enablePISPMode: ', this._enablePISPMode); } /** * Initializes the requestToPayTransfer model @@ -93,7 +106,9 @@ class OutboundRequestToPayTransferModel { transitions: [ { name: 'requestQuote', from: 'start', to: 'quoteReceived' }, { name: 'requestOTP', from: 'quoteReceived', to: 'otpReceived' }, + { name: 'requestAuthorization', from: 'quoteReceived', to: 'authorizationReceived' }, { name: 'executeTransfer', from: 'otpReceived', to: 'succeeded' }, + { name: 'executeAuthorizedTransfer', from: 'authorizationReceived', to: 'succeeded' }, { name: 'error', from: '*', to: 'errored' }, ], methods: { @@ -122,7 +137,10 @@ class OutboundRequestToPayTransferModel { // next transition is to requestQuote await this.stateMachine.requestQuote(); this._logger.log(`Quote received for transfer ${this.data.transferId}`); + + if(this.stateMachine.state === 'quoteReceived' && this.data.initiatorType === 'BUSINESS' && !this._autoAcceptR2PBusinessQuotes) { + // kick-off postAuthorizations here for PISP flow //we break execution here and return the quote response details to allow asynchronous accept or reject //of the quote await this._save(); @@ -131,19 +149,31 @@ class OutboundRequestToPayTransferModel { break; case 'quoteReceived': - // next transition is requestOTP - await this.stateMachine.requestOTP(); - if(this.data.initiatorType !== 'BUSINESS') { - this._logger.log(`OTP received for transactionId: ${this.data.requestToPayTransactionId} and transferId: ${this.data.transferId}`); - if(this.stateMachine.state === 'otpReceived' && !this._autoAcceptR2PDeviceOTP) { - //we break execution here and return the otp response details to allow asynchronous accept or reject - //of the quote - await this._save(); - return this.getResponse(); + // decide PISP or OTP flow + if (this._enablePISPMode) { + await this.stateMachine.requestAuthorization(); + await this._save(); + // let executeTransfer in recursive call of run() + } else { + await this.stateMachine.requestOTP(); + if (this.data.initiatorType !== 'BUSINESS') { + this._logger.log(`OTP received for transactionId: ${this.data.requestToPayTransactionId} and transferId: ${this.data.transferId}`); + if (this.stateMachine.state === 'otpReceived' && !this._autoAcceptR2PDeviceOTP) { + //we break execution here and return the otp response details to allow asynchronous accept or reject + //of the quote + await this._save(); + return this.getResponse(); + } } } break; - + + case 'authorizationReceived': + // next transition is executeTransfer + await this.stateMachine.executeAuthorizedTransfer(); + this._logger.log(`Transfer ${this.data.transferId} has been completed`); + break; + case 'otpReceived': // next transition is executeTransfer await this.stateMachine.executeTransfer(); @@ -163,7 +193,7 @@ class OutboundRequestToPayTransferModel { } // now call ourslves recursively to deal with the next transition - this._logger.log(`RequestToPay Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`); + this._logger.log(`RequestToPay Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recursing to handle next transition.`); return this.run(); } catch(err) { @@ -209,11 +239,16 @@ class OutboundRequestToPayTransferModel { // request a quote return this._requestQuote(); + case 'requestAuthorization': + // request an OTP + return this._requestAuthorization(); + case 'requestOTP': // request an OTP return this._requestOTP(); case 'executeTransfer': + case 'executeAuthorizedTransfer': // prepare a transfer and wait for fulfillment return this._executeTransfer(); @@ -228,7 +263,7 @@ class OutboundRequestToPayTransferModel { } /** - * This method is used to communicate back to the Payee that a rejection is being + * This method is used to communicate back to the Payee that a rejection is being * sent because the OTP did not match. */ async rejectRequestToPay() { @@ -239,7 +274,7 @@ class OutboundRequestToPayTransferModel { const response = { status : `${this.data.requestToPayTransactionId} has been REJECTED` }; - return JSON.stringify(response); + return JSON.stringify(response); } @@ -465,8 +500,54 @@ class OutboundRequestToPayTransferModel { }); } + async _requestAuthorization() { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + // let use here OutboundAuthorizationsModel which allows to do synchronous call to get Authorization from PISP + this._logger.log('OutboundRequestToPayTransferModel._requestAuthorization'); + // prepare request + const authorizationsRequest = { + // here is hardcoded address of PISP because in this flow there is no way to get it so we are mocking it out + // take a look in thirdparty-scheme-adapter PISPTransactionModel(WIP) + toParticipantId: 'pisp', + authenticationType: 'U2F', + retriesLeft: '1', + amount: { + currency: 'USD', + amount: this.data.amount + }, + transactionId: this.data.transferId, + transactionRequestId: this.data.requestToPayTransactionId, + quote: { ...this.data.quoteResponse }, + }; + + const modelConfig = { ...this._config }; + + const cacheKey = `post_authorizations_${authorizationsRequest.transactionRequestId}`; + + // use the authorizations model to execute asynchronous stages with the switch + const model = await OutboundAuthorizationsModel.create(authorizationsRequest, cacheKey, modelConfig); + + // run model's workflow + + this.data.authorizationResponse = await model.run(); + // here is POC: happy flow + // the authorizationResponse should be analyzed + // and the pinValue should be validated but this is out of the scope of this POC + this._logger.push({ authorizationResponse: this.data.authorizationResponse }).log('authorizationResponse received'); + + // let call backend service which will validate pinValue + // TODO: add `/validate-authorization` path to mojaloop_simulator + // const validateResponse = await this._backendRequests.validateAuthorization(this.data.authorizationResponse); + // if (validateResponse.validationResult !== 'OK') { + // return reject(new Error('Invalid Authorization of Transaction')); + // } + resolve(this.data.authorizationResponse); + }); + } + /** - * Sends request for + * Sends request for * Starts the quote resolution process by sending a POST /quotes request to the switch; * then waits for a notification from the cache that the quote response has been received */ @@ -475,7 +556,7 @@ class OutboundRequestToPayTransferModel { return new Promise(async (resolve, reject) => { if( this.data.initiatorType && this.data.initiatorType === 'BUSINESS') return resolve(); - + // listen for events on the quoteId const otpKey = `otp_${this.data.requestToPayTransactionId}`; @@ -496,9 +577,9 @@ class OutboundRequestToPayTransferModel { const otpResponseBody = otpResponse.data; this._logger.push({ otpResponseBody }).log('OTP response received'); - + this.data.otpResponse = otpResponseBody; - + return resolve(otpResponse); } catch(err) { @@ -582,7 +663,7 @@ class OutboundRequestToPayTransferModel { return quote; } - + /** * Executes a transfer * Starts the transfer process by sending a POST /transfers (prepare) request to the switch; @@ -822,6 +903,10 @@ class OutboundRequestToPayTransferModel { resp.currentState = requestToPayTransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE; break; + case 'authorizationReceived': + resp.currentState = requestToPayTransferStateEnum.WAITING_FOR_AUTHORIZATION_ACCEPTANCE; + break; + case 'otpReceived': resp.currentState = requestToPayTransferStateEnum.WAITING_FOR_OTP_ACCEPTANCE; break; @@ -881,7 +966,7 @@ class OutboundRequestToPayTransferModel { } - + } diff --git a/src/lib/model/OutboundThirdpartyTransactionModel.js b/src/lib/model/OutboundThirdpartyTransactionModel.js new file mode 100644 index 000000000..71b6a49e9 --- /dev/null +++ b/src/lib/model/OutboundThirdpartyTransactionModel.js @@ -0,0 +1,287 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Sridhar Voruganti - sridhar.voruganti@modusbox.com * + **************************************************************************/ + +'use strict'; + +const util = require('util'); +const { uuid } = require('uuidv4'); +const PSM = require('./common').PersistentStateMachine; +const ThirdpartyRequests = require('@mojaloop/sdk-standard-components').ThirdpartyRequests; + + +const specStateMachine = { + transitions: [ + { name: 'getThirdPartyTransaction', from: 'getTransaction', to: 'transactionSuccess' }, + { name: 'postThirdPartyTransaction', from: 'postTransaction', to: 'transactionSuccess' }, + { name: 'error', from: '*', to: 'errored' }, + ], + methods: { + // workflow methods + run, + getResponse, + + // specific transitions handlers methods + onGetThirdPartyTransaction, + onPostThirdPartyTransaction + } +}; + +const mapCurrentState = { + getTransaction: 'WAITING', + postTransaction: 'WAITING', + transactionSuccess: 'COMPLETED', + errored: 'ERROR_OCCURRED' +}; + +function notificationChannel(id) { + // mvp validation + if (!(id && id.toString().length > 0)) { + throw new Error('OutboundThirdpartyTransactionModel.notificationChannel: \'id\' parameter is required'); + } + + // channel name + return `3ptrxnreq_${id}`; +} + +async function publishNotifications(cache, id, value) { + const channel = notificationChannel(id); + return cache.publish(channel, value); +} + +/** + * injects the config into state machine data + * so it will be accessible to on transition notification handlers via `this.handlersContext` + * + * @param {Object} config - config to be injected into state machine data + * @param {Object} specStateMachine - specState machine to be altered + * @returns {Object} - the altered specStateMachine + */ +function injectHandlersContext(config, specStateMachine) { + return { + ...specStateMachine, + data: { + handlersContext: { + config, // injects config property + requests: new ThirdpartyRequests({ + logger: config.logger, + peerEndpoint: config.peerEndpoint, + alsEndpoint: config.alsEndpoint, + quotesEndpoint: config.quotesEndpoint, + transfersEndpoint: config.transfersEndpoint, + transactionRequestsEndpoint: config.transactionRequestsEndpoint, + dfspId: config.dfspId, + tls: config.outbound.tls, + jwsSign: config.jwsSign, + jwsSignPutParties: config.jwsSignPutParties, + jwsSigningKey: config.jwsSigningKey, + wso2: config.wso2, + }) + } + } + }; +} + + +/** + * creates a new instance of state machine specified in specStateMachine ^ + * + * @param {Object} data - payload data + * @param {String} key - the cache key where state machine will store the payload data after each transition + * @param {Object} config - the additional configuration for transition handlers + */ +async function create(data, key, config) { + + if(!data.hasOwnProperty('transactionRequestId')) { + data.transactionRequestId = uuid(); + } + + const spec = injectHandlersContext(config, specStateMachine); + return PSM.create(data, config.cache, key, config.logger, spec); +} + + +/** + * loads state machine from cache by given key and specify the additional config for transition handlers + * @param {String} key - the cache key used to retrieve the state machine from cache + * @param {Object} config - the additional configuration for transition handlers + */ +async function loadFromCache(key, config) { + const customCreate = async (data, cache, key /**, logger, stateMachineSpec **/) => create(data, key, config); + return PSM.loadFromCache(config.cache, key, config.logger, specStateMachine, customCreate); +} + +/** + * runs the workflow + */ +async function run() { + const { data, logger } = this.context; + try { + // run transitions based on incoming state + switch(data.currentState) { + case 'getTransaction': + await this.getThirdPartyTransaction(); + logger.log(`GET Thirdparty transaction requested for ${data.transactionRequestId}, currentState: ${data.currentState}`); + break; + + case 'postTransaction': + await this.postThirdPartyTransaction(); + logger.log(`POST Thirdparty transaction requested for ${data.transactionRequestId}, currentState: ${data.currentState}`); + break; + + case 'transactionSuccess': + // all steps complete so return + logger.log('ThirdpartyTransaction completed successfully'); + return this.getResponse(); + + case 'errored': + // stopped in errored state + logger.log('State machine in errored state'); + return; + } + + logger.log(`Thirdparty request model state machine transition completed in state: ${this.state}. Recursing to handle next transition.`); + return this.run(); + + } catch (err) { + logger.log(`Error running ThirdPartyTransaction model: ${util.inspect(err)}`); + + // as this function is recursive, we don't want to error the state machine multiple times + if(data.currentState !== 'errored') { + // err should not have a transactionState property here! + if(err.transactionState) { + logger.log('State machine is broken'); + } + // transition to errored state + await this.error(err); + + // avoid circular ref between transactionState.lastError and err + err.transactionState = JSON.parse(JSON.stringify(this.getResponse())); + } + throw err; + } +} + +async function onGetThirdPartyTransaction() { + const { data, cache, logger } = this.context; + const { requests } = this.handlersContext; + const transferKey = notificationChannel(data.transactionRequestId); + let subId; + + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + try { + + subId = await cache.subscribe(transferKey, (cn, message, subId) => { + try { + const parsed = JSON.parse(message); + this.context.data = { + ...parsed.data, + currentState: this.state + }; + resolve(); + } catch(err) { + reject(err); + } finally { + if(subId) { + cache.unsubscribe(subId); + } + } + }); + + // Not sure what should be the destination FSP so using null for now. + const res = await requests.getThirdpartyRequestsTransactions(data.transactionRequestId, null); + logger.push({ res }).log('Thirdparty transaction request sent to peer'); + + } catch(error) { + logger.push(error).error('GET thirdparty transaction request error'); + if(subId) { + cache.unsubscribe(subId); + } + reject(error); + } + }); +} + +async function onPostThirdPartyTransaction() { + const { data, cache, logger } = this.context; + const { requests } = this.handlersContext; + const transferKey = notificationChannel(data.transactionRequestId); + let subId; + + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + try { + + subId = await cache.subscribe(transferKey, (cn, message, subId) => { + try { + const parsed = JSON.parse(message); + this.context.data = { + ...parsed.data, + currentState: this.state + }; + resolve(); + } catch(err) { + reject(err); + } finally { + if(subId) { + cache.unsubscribe(subId); + } + } + }); + + const request = { + ...data + }; + + // Request is routed to switch and then to the payer's fsp. + const res = await requests.postThirdpartyRequestsTransactions(request, data.payer.partyIdInfo.fspId); + logger.push({ res }).log('Thirdparty transaction request sent to peer'); + + } catch(error) { + logger.push(error).error('GET thirdparty transaction request error'); + if(subId) { + cache.unsubscribe(subId); + } + reject(error); + } + }); +} + +/** + * Returns an object representing the final state of the transaction request suitable for the outbound API + * + * @returns {object} - Response representing the result of the transaction request process + */ +function getResponse() { + const { data, logger } = this.context; + let resp = { ...data }; + + // project some of our internal state into a more useful + // representation to return to the SDK API consumer + resp.currentState = mapCurrentState[data.currentState]; + + // handle unexpected state + if(!resp.currentState) { + logger.log(`OutboundThirdpartyTransaction model response being returned from an unexpected state: ${data.currentState}. Returning ERROR_OCCURRED state`); + resp.currentState = mapCurrentState.errored; + } + + return resp; +} + +module.exports = { + create, + loadFromCache, + notificationChannel, + publishNotifications, + + // exports for testing purposes + mapCurrentState +}; diff --git a/src/lib/model/OutboundTransfersModel.js b/src/lib/model/OutboundTransfersModel.js index 694601066..2cd7d6db0 100644 --- a/src/lib/model/OutboundTransfersModel.js +++ b/src/lib/model/OutboundTransfersModel.js @@ -48,16 +48,18 @@ class OutboundTransfersModel { alsEndpoint: config.alsEndpoint, quotesEndpoint: config.quotesEndpoint, transfersEndpoint: config.transfersEndpoint, + transactionRequestsEndpoint: config.transactionRequestsEndpoint, dfspId: config.dfspId, - tls: config.tls, + tls: config.outbound.tls, jwsSign: config.jwsSign, jwsSignPutParties: config.jwsSignPutParties, jwsSigningKey: config.jwsSigningKey, - wso2Auth: config.wso2Auth + wso2: config.wso2, }); this._ilp = new Ilp({ - secret: config.ilpSecret + secret: config.ilpSecret, + logger: this._logger, }); } @@ -118,6 +120,10 @@ class OutboundTransfersModel { this.data.currentState = 'start'; } + if(!this.data.hasOwnProperty('initiatedTimestamp')) { + this.data.initiatedTimestamp = new Date().toISOString(); + } + this._initStateMachine(this.data.currentState); } @@ -779,6 +785,7 @@ class OutboundTransfersModel { case 'errored': // stopped in errored state + await this._save(); this._logger.log('State machine in errored state'); return; } @@ -801,6 +808,7 @@ class OutboundTransfersModel { // avoid circular ref between transferState.lastError and err err.transferState = JSON.parse(JSON.stringify(this.getResponse())); + await this._save(); } throw err; } diff --git a/src/lib/model/PartiesModel.js b/src/lib/model/PartiesModel.js new file mode 100644 index 000000000..8fdedebde --- /dev/null +++ b/src/lib/model/PartiesModel.js @@ -0,0 +1,260 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; +const util = require('util'); + +const PSM = require('./common').PersistentStateMachine; +const MojaloopRequests = require('@mojaloop/sdk-standard-components').MojaloopRequests; + +const specStateMachine = { + init: 'start', + transitions: [ + { name: 'init', from: 'none', to: 'start' }, + { name: 'requestPartiesInformation', from: 'start', to: 'succeeded' }, + { name: 'error', from: '*', to: 'errored' }, + ], + methods: { + // workflow methods + run, + getResponse, + + // specific transitions handlers methods + onRequestPartiesInformation, + } +}; + +/** + * @name run + * @description run the workflow logic + * @param {string} type - the party type + * @param {string} id - the party id + * @param {string} [subId] - the optional party subId + * @returns {Object} - the http response payload + */ +async function run(type, id, subId) { + // input validation + const channel = channelName(type, id, subId); + if (channel.indexOf('-undefined-') != -1) { + throw new Error('PartiesModel.run required at least two string arguments: \'type\' and \'id\''); + } + + const { data, logger } = this.context; + try { + // run transitions based on incoming state + switch(data.currentState) { + case 'start': + // the first transition is requestPartiesInformation + await this.requestPartiesInformation(type, id, subId); + // don't await to finish the save + this.saveToCache(); + logger.log(`Party information requested for /${type}/${id}/${subId}, currentState: ${data.currentState}`); + + // eslint-disable-next-line no-fallthrough + case 'succeeded': + // all steps complete so return + logger.log('Party information retrieved successfully'); + return this.getResponse(); + + case 'errored': + // stopped in errored state + logger.log('State machine in errored state'); + return; + } + } catch (err) { + logger.log(`Error running Parties model: ${util.inspect(err)}`); + + // as this function is recursive, we don't want to error the state machine multiple times + if(data.currentState !== 'errored') { + // err should not have a requestPartiesInformationState property here! + if(err.requestPartiesInformationState) { + logger.log('State machine is broken'); + } + // transition to errored state + await this.error(err); + + // avoid circular ref between requestPartiesInformationState.lastError and err + err.requestPartiesInformationState = JSON.parse(JSON.stringify(this.getResponse())); + } + throw err; + } +} + +const mapCurrentState = { + start: 'WAITING_FOR_REQUEST_PARTY_INFORMATION', + succeeded: 'COMPLETED', + errored: 'ERROR_OCCURRED' +}; + +/** + * @name getResponse + * @description returns the http response payload depending on which state machine is + * @returns {Object} - the http response payload + */ +function getResponse() { + const { data, logger } = this.context; + let resp = { ...data }; + + // project some of our internal state into a more useful + // representation to return to the SDK API consumer + resp.currentState = mapCurrentState[data.currentState]; + + // handle unexpected state + if(!resp.currentState) { + logger.error(`Parties model response being returned from an unexpected state: ${data.currentState}. Returning ERROR_OCCURRED state`); + resp.currentState = mapCurrentState.errored; + } + + return resp; +} +/** + * @name onRequestPartiesInformation + * @description generates the pub/sub channel name + * @param {string} type - the party type + * @param {string} id - the party id + * @param {string} [subId] - the optional party subId + * @returns {string} - the pub/sub channel name + */ +async function onRequestPartiesInformation(fsm, type, id, subId) { + const { cache, logger } = this.context; + const { requests } = this.handlersContext; + logger.push({ type, id, subId }).error('onReqeustPartiesInformation - arguments'); + const channel = channelName(type, id, subId); + let sid; + + // eslint-disable-next-line no-async-promise-executor + return new Promise( async(resolve, reject) => { + try { + // in InboundServer/handlers is implemented putPartiesById handler + sid = await cache.subscribe(channel, async (channel, message, sid) => { + // unsubscribe first + cache.unsubscribe(channel, sid); + + // protect against malformed JSON message + try { + const parsed = JSON.parse(message); + this.context.data = { + ...parsed, + currentState: this.state + }; + resolve(); + } catch(err) { + reject(err); + } + }); + + // GET /parties request to the switch + const res = await requests.getParties(type, id, subId); + + logger.push({ res }).log(' RequestPartiesInformation sent to peer'); + + } catch(error) { + logger.push(error).error('RequestPartiesInformation error'); + cache.unsubscribe(channel, sid); + reject(error); + } + }); +} + + +/** + * @name channelName + * @description generates the pub/sub channel name + * @param {string} type - the party type + * @param {string} id - the party id + * @param {string} [subId] - the optional party subId + * @returns {string} - the pub/sub channel name + */ +function channelName(type, id, subId) { + const tokens = ['parties', type, id, subId]; + return tokens.map(x => `${x}`).join('-'); +} + + +/** + * @name generateKey + * @description generates the cache key used to store state machine + * @param {string} type - the party type + * @param {string} id - the party id + * @param {string} [subId] - the optional party subId + * @returns {string} - the cache key + */ +function generateKey(type, id, subId) { + return `key-${channelName(type, id, subId)}`; +} + + +/** + * @name injectHandlersContext + * @description injects the config into state machine data, so it will be accessible to on transition notification handlers via `this.handlersContext` + * @param {Object} config - config to be injected into state machine data + * @returns {Object} - the altered specStateMachine + */ +function injectHandlersContext(config) { + return { + ...specStateMachine, + data: { + handlersContext: { + config: { ...config }, // injects config property + requests: new MojaloopRequests({ + logger: config.logger, + peerEndpoint: config.peerEndpoint, + alsEndpoint: config.alsEndpoint, + quotesEndpoint: config.quotesEndpoint, + transfersEndpoint: config.transfersEndpoint, + transactionRequestsEndpoint: config.transactionRequestsEndpoint, + dfspId: config.dfspId, + tls: config.outbound.tls, + jwsSign: config.jwsSign, + jwsSignPutParties: config.jwsSignPutParties, + jwsSigningKey: config.jwsSigningKey, + wso2: config.wso2, + }) + } + } + }; +} + + +/** + * @name create + * @description creates a new instance of state machine specified in specStateMachine ^ + * @param {Object} data - payload data + * @param {String} key - the cache key where state machine will store the payload data after each transition + * @param {Object} config - the additional configuration for transition handlers + */ +async function create(data, key, config) { + const spec = injectHandlersContext(config, specStateMachine); + return PSM.create(data, config.cache, key, config.logger, spec); +} + + +/** + * @name loadFromCache + * @description loads state machine from cache by given key and specify the additional config for transition handlers + * @param {String} key - the cache key used to retrieve the state machine from cache + * @param {Object} config - the additional configuration for transition handlers + */ +async function loadFromCache(key, config) { + const customCreate = async (data, _cache, key) => create(data, key, config); + return PSM.loadFromCache(config.cache, key, config.logger, specStateMachine, customCreate); +} + + +module.exports = { + channelName, + create, + generateKey, + loadFromCache, + + // exports for testing purposes + mapCurrentState +}; + diff --git a/src/lib/model/ProxyModel/index.js b/src/lib/model/ProxyModel/index.js index 35549735b..0a5f489ea 100644 --- a/src/lib/model/ProxyModel/index.js +++ b/src/lib/model/ProxyModel/index.js @@ -36,7 +36,7 @@ class ProxyModel { logger: this._logger, peerEndpoint: config.peerEndpoint, dfspId: config.dfspId, - tls: config.tls, + tls: config.outbound.tls, jwsSign: config.jwsSign, jwsSigningKey: config.jwsSigningKey, wso2Auth: config.wso2Auth diff --git a/src/lib/model/common.js b/src/lib/model/common/BackendError.js similarity index 72% rename from src/lib/model/common.js rename to src/lib/model/common/BackendError.js index cf21c3ae3..8a5a6577b 100644 --- a/src/lib/model/common.js +++ b/src/lib/model/common/BackendError.js @@ -17,22 +17,10 @@ class BackendError extends Error { } toJSON() { - const ret = { - httpStatusCode: this.httpStatusCode - }; - - //copy across any other properties - for(let prop in this) { - if(this.hasOwnProperty(prop)) { - ret[prop] = this[prop]; - } - } - - return ret; + // return shallow clone of `this`, from `this` are only taken enumerable owned properties + return Object.assign({}, this); + } } - -module.exports = { - BackendError, -}; +module.exports = BackendError; diff --git a/src/lib/model/common/PersistentStateMachine.js b/src/lib/model/common/PersistentStateMachine.js new file mode 100644 index 000000000..5e5b4c74a --- /dev/null +++ b/src/lib/model/common/PersistentStateMachine.js @@ -0,0 +1,93 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; +const StateMachine = require('javascript-state-machine'); + +async function saveToCache() { + const { data, cache, key, logger } = this.context; + try { + const res = await cache.set(key, data); + logger.push({ res }).log(`Persisted model in cache: ${key}`); + } + catch(err) { + logger.push({ err }).log(`Error saving model: ${key}`); + throw err; + } +} + +async function onAfterTransition(transition) { + const { logger } = this.context; + logger.log(`State machine transitioned '${transition.transition}': ${transition.from} -> ${transition.to}`); + this.context.data.currentState = transition.to; +} + +function onPendingTransition(transition) { + // allow transitions to 'error' state while other transitions are in progress + if(transition !== 'error') { + throw new Error(`Transition '${transition}' requested while another transition is in progress.`); + } +} + +async function create(data, cache, key, logger, stateMachineSpec ) { + let initState = stateMachineSpec.init || 'init'; + + if(!data.hasOwnProperty('currentState')) { + data.currentState = initState; + } else { + initState = stateMachineSpec.init = data.currentState; + } + + stateMachineSpec.data = Object.assign( + stateMachineSpec.data || {}, + { + context: { + data, cache, key, logger + } + } + ); + + stateMachineSpec.methods = Object.assign( + stateMachineSpec.methods || {}, + { + onAfterTransition, + onPendingTransition, + saveToCache + } + ); + + const stateMachine = new StateMachine(stateMachineSpec); + await stateMachine[initState]; + return stateMachine; +} + + +async function loadFromCache(cache, key, logger, stateMachineSpec, optCreate) { + try { + const data = await cache.get(key); + if(!data) { + throw new Error(`No cached data found for: ${key}`); + } + logger.push({ cache: data }).log('data loaded from cache'); + + // use delegation to allow customization of 'create' + const createPSM = optCreate || create; + return createPSM(data, cache, key, logger, stateMachineSpec); + } + catch(err) { + logger.push({ err }).log(`Error loading data: ${key}`); + throw err; + } +} + +module.exports = { + loadFromCache, + create +}; diff --git a/src/lib/model/common/index.js b/src/lib/model/common/index.js new file mode 100644 index 000000000..3f90c1fdc --- /dev/null +++ b/src/lib/model/common/index.js @@ -0,0 +1,18 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; +const BackendError = require('./BackendError'); +const PersistentStateMachine = require('./PersistentStateMachine'); + +module.exports = { + BackendError, + PersistentStateMachine +}; diff --git a/src/lib/model/index.js b/src/lib/model/index.js index 925e4d5c7..0a6be6258 100644 --- a/src/lib/model/index.js +++ b/src/lib/model/index.js @@ -13,19 +13,31 @@ const InboundTransfersModel = require('./InboundTransfersModel.js'); const OutboundTransfersModel = require('./OutboundTransfersModel.js'); +const OutboundBulkQuotesModel = require('./OutboundBulkQuotesModel'); +const OutboundBulkTransfersModel = require('./OutboundBulkTransfersModel.js'); const OutboundRequestToPayTransferModel = require('./OutboundRequestToPayTransferModel.js'); const AccountsModel = require('./AccountsModel'); const ProxyModel = require('./ProxyModel'); const OutboundRequestToPayModel = require('./OutboundRequestToPayModel'); -const { BackendError } = require('./common'); - +const OutboundAuthorizationsModel = require('./OutboundAuthorizationsModel'); +const InboundThirdpartyTransactionModel = require('./InboundThirdpartyTransactionModel'); +const OutboundThirdpartyTransactionModel = require('./OutboundThirdpartyTransactionModel'); +const { BackendError, PersistentStateMachine } = require('./common'); +const PartiesModel = require('./PartiesModel'); module.exports = { + AccountsModel, + BackendError, + OutboundBulkQuotesModel, + OutboundBulkTransfersModel, + OutboundRequestToPayTransferModel, + OutboundRequestToPayModel, InboundTransfersModel, OutboundTransfersModel, - OutboundRequestToPayTransferModel, - AccountsModel, ProxyModel, - BackendError, - OutboundRequestToPayModel + OutboundAuthorizationsModel, + PersistentStateMachine, + InboundThirdpartyTransactionModel, + OutboundThirdpartyTransactionModel, + PartiesModel, }; diff --git a/src/lib/model/lib/requests/backendRequests.js b/src/lib/model/lib/requests/backendRequests.js index 3311bbf7a..70a835bcd 100644 --- a/src/lib/model/lib/requests/backendRequests.js +++ b/src/lib/model/lib/requests/backendRequests.js @@ -37,6 +37,19 @@ class BackendRequests { this.backendEndpoint = `${this.transportScheme}://${config.backendEndpoint}`; } + /** + * Executes a /signchallenge request by passing authorization request details + * + * @returns {object} - JSON response body if one was received + */ + async getSignedChallenge(authorizationReq) { + return this._post('signchallenge', authorizationReq); + } + + async validateAuthorization(authorizationResponse) { + return this._post('validate-authorization', authorizationResponse); + } + /** * Executes a GET /otp request for the specified transaction request id * @@ -98,6 +111,54 @@ class BackendRequests { return this._post('transactionrequests', transactionRequest); } + /** + * Executes a POST /bulkQuotes request for the specified bulk quotes request + * + * @returns {object} - JSON response body if one was received + */ + async postBulkQuotes(bulkQuotesRequest) { + return this._post('bulkQuotes', bulkQuotesRequest); + } + + /** + * Executes a GET /bulkQuotes/{ID} request for the specified bulk quote ID + * + * @returns {object} - JSON response body if one was received + */ + async getBulkQuotes(bulkQuoteId) { + const url = `bulkQuotes/${bulkQuoteId}`; + return this._get(url); + } + + /** + * Executes a POST /bulkTransfers request for the specified bulk transfer prepare + * + * @returns {object} - JSON response body if one was received + */ + async postBulkTransfers(prepare) { + return this._post('bulkTransfers', prepare); + } + + /** + * Executes a GET /bulkTransfers/{ID} request for the specified bulk transfer ID + * + * @returns {object} - JSON response body if one was received + */ + async getBulkTransfers(bulkTransferId) { + const url = `bulkTransfers/${bulkTransferId}`; + return this._get(url); + } + + /** + * Executes a PUT /transfers/{ID} request to forward notification for success + * + * @returns {object} - JSON response body if one was received + */ + + async putTransfersNotification(notifcation, transferId) { + const url = `transfers/${transferId}`; + return this._put(url, notifcation); + } /** * Utility function for building outgoing request headers as required by the mojaloop api spec @@ -139,7 +200,7 @@ class BackendRequests { method: 'PUT', uri: buildUrl(this.backendEndpoint, url), headers: this._buildHeaders(), - body: JSON.stringify(body), + body: JSON.stringify(body) }; try { diff --git a/src/lib/model/lib/shared/index.js b/src/lib/model/lib/shared/index.js index baf80f88d..a25e4d852 100644 --- a/src/lib/model/lib/shared/index.js +++ b/src/lib/model/lib/shared/index.js @@ -25,7 +25,6 @@ const internalPartyToMojaloopParty = (internal, fspId) => { fspId: fspId } }; - if (internal.extensionList) { party.partyIdInfo.extensionList = { extension: internal.extensionList @@ -53,6 +52,12 @@ const internalPartyToMojaloopParty = (internal, fspId) => { party.merchantClassificationCode = internal.merchantClassificationCode; } + if (internal.accounts) { + party.accounts = { + account: internal.accounts + }; + } + return party; }; @@ -96,6 +101,10 @@ const mojaloopPartyToInternalParty = (external) => { } } + if(external.accounts){ + internal.accounts = external.accounts.account; + } + return internal; }; @@ -275,6 +284,200 @@ const mojaloopTransactionRequestToInternal = (external) => { return internal; }; +/** + * Projects a Mojaloop API spec bulk quote request to internal form + * + * @returns {object} - the internal form bulk quote request + */ +const mojaloopBulkQuotesRequestToInternal = (external) => { + const internal = { + bulkQuoteId: external.bulkQuoteId, + from: mojaloopPartyToInternalParty(external.payer), + }; + + if (external.geoCode) { + internal.geoCode = external.geoCode; + } + + if (external.expiration) { + internal.expiration = external.expiration; + } + + if (external.extensionList) { + internal.extensionList = external.extensionList.extension; + } + + const internalIndividualQuotes = external.individualQuotes.map(quote => { + const internalQuote = { + quoteId: quote.quoteId, + transactionId: quote.transactionId, + to: mojaloopPartyToInternalParty(quote.payee), + amountType: quote.amountType, + amount: quote.amount.amount, + currency: quote.amount.currency, + transactionType: quote.transactionType.scenario, + initiator: quote.transactionType.initiator, + initiatorType: quote.transactionType.initiatorType + }; + + if (quote.fees) { + internal.feesAmount = quote.fees.amount; + internal.feesCurrency = quote.fees.currency; + } + + if (quote.geoCode) { + internal.geoCode = quote.geoCode; + } + + if (quote.note) { + internal.note = quote.note; + } + + return internalQuote; + }); + + internal.individualQuotes = internalIndividualQuotes; + + return internal; +}; + +/** + * Converts an internal bulk quotes response to mojaloop form + * + * @returns {object} + */ +const internalBulkQuotesResponseToMojaloop = (internal) => { + const individualQuoteResults = internal.individualQuoteResults.map((quote) => { + const externalQuote = { + quoteId: quote.quoteId, + transferAmount: { + amount: quote.transferAmount, + currency: quote.transferAmountCurrency, + }, + ilpPacket: quote.ilpPacket, + condition: quote.ilpCondition + }; + + if (quote.payeeReceiveAmount) { + externalQuote.payeeReceiveAmount = { + amount: quote.payeeReceiveAmount, + currency: quote.payeeReceiveAmountCurrency + }; + } + + if (quote.payeeFspFeeAmount) { + externalQuote.payeeFspFee = { + amount: quote.payeeFspFeeAmount, + currency: quote.payeeFspFeeAmountCurrency + }; + } + + if (quote.payeeFspCommissionAmount) { + externalQuote.payeeFspCommission = { + amount: quote.payeeFspCommissionAmount, + currency: quote.payeeFspCommissionAmountCurrency + }; + } + + return externalQuote; + }); + const external = { + individualQuoteResults, + expiration: internal.expiration, + }; + + if (internal.geoCode) { + external.geoCode = internal.geoCode; + } + + if (internal.extensionList) { + external.extensionList = internal.extensionList; + } + + return external; +}; + +/** + * Converts a mojaloop bulk transfer prepare request to internal form + * + * @returns {object} + */ +const mojaloopBulkPrepareToInternalBulkTransfer = (external, bulkQuotes, ilp) => { + let internal = null; + if (bulkQuotes) { + // create a map of internal individual quotes payees indexed by quotedId, for faster lookup + const internalQuotesPayeesByQuoteId = {}; + + for (const quote of bulkQuotes.internalRequest.individualQuotes) { + internalQuotesPayeesByQuoteId[quote.quoteId] = quote.to; + } + + // create a map of external individual transfers indexed by quotedId, for faster lookup + const externalTransferIdsByQuoteId = {}; + + for (const transfer of external.individualTransfers) { + const transactionObject = ilp.getTransactionObject(transfer.ilpPacket); + externalTransferIdsByQuoteId[transactionObject.quoteId] = transfer.transferId; + } + + internal = { + bulkTransferId: external.bulkTransferId, + bulkQuotes: bulkQuotes.response, + from: bulkQuotes.internalRequest.from, + }; + + internal.individualTransfers = bulkQuotes.request.individualQuotes.map((quote) => ({ + transferId: externalTransferIdsByQuoteId[quote.quoteId], + to: internalQuotesPayeesByQuoteId[quote.quoteId], + amountType: quote.amountType, + currency: quote.amount.currency, + amount: quote.amount.amount, + transactionType: quote.transactionType.scenario, + note: quote.note + })); + } else { + internal = { + bulkTransferId: external.bulkTransferId, + individualTransfers: external.individualTransfers.map((transfer) => ({ + transferId: transfer.transferId, + currency: transfer.transferAmount.currency, + amount: transfer.transferAmount.amount, + })) + }; + } + + return internal; +}; +/** + * Converts a mojaloop authorizationsReq data to internal form + * + * @returns {object} + */ +const mojaloopAuthorizationsReqToInternal = (external) => { + let internal = external; + //TODO: conversion required ? + return internal; +}; + +/** + * Converts an internal authorizations response to mojaloop form + * + * @returns {object} + */ +const internalAuthorizationsResponseToMojaloop = (internal) => { + //TODO: conversion required or not ? + const external = { + authenticationInfo: { + authentication: 'U2F', + authenticationValue: { + pinValue: internal.pinValue, + counter: internal.counter + } + }, + responseType: 'ENTERED' + }; + return external; +}; module.exports = { internalPartyToMojaloopParty, @@ -284,5 +487,10 @@ module.exports = { mojaloopPartyIdInfoToInternalPartyIdInfo, mojaloopQuoteRequestToInternal, mojaloopPrepareToInternalTransfer, - mojaloopTransactionRequestToInternal + mojaloopTransactionRequestToInternal, + mojaloopAuthorizationsReqToInternal, + internalAuthorizationsResponseToMojaloop, + mojaloopBulkQuotesRequestToInternal, + internalBulkQuotesResponseToMojaloop, + mojaloopBulkPrepareToInternalBulkTransfer, }; diff --git a/src/lib/randomphrase/randomphrase.js b/src/lib/randomphrase/randomphrase.js index ab8eb8f04..58ad1c305 100644 --- a/src/lib/randomphrase/randomphrase.js +++ b/src/lib/randomphrase/randomphrase.js @@ -5,12 +5,11 @@ * specified in the corresponding source code repository. * * * * ORIGINAL AUTHOR: * - * James Bush - james.bush@modusbox.com * + * Matt Kingston - matt.kingston@modusbox.com * **************************************************************************/ 'use strict'; - const words = require('./words.json'); const randomEl = arr => arr[Math.floor(Math.random() * arr.length)]; diff --git a/src/lib/validate/index.js b/src/lib/validate/index.js index 6e1a54a1c..d8db67ac3 100644 --- a/src/lib/validate/index.js +++ b/src/lib/validate/index.js @@ -194,7 +194,7 @@ class Validator { throw err; } - err = new Error(util.format('Request failed validation', validationResult)); + err = new Error(util.format('Request failed validation', validationResult)); Object.assign(err, firstError); throw err; } diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 000000000..4c7733ea6 --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,8472 @@ +{ + "name": "@mojaloop/sdk-scheme-adapter", + "version": "11.10.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "dev": true, + "requires": { + "@babel/types": "^7.12.11" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@eslint/eslintrc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@internal/cache": { + "version": "file:lib/cache", + "requires": { + "redis": "^2.8.0" + } + }, + "@internal/check": { + "version": "file:lib/check" + }, + "@internal/model": { + "version": "file:lib/model", + "requires": { + "@internal/requests": "file:lib/model/lib/requests", + "@internal/shared": "file:lib/model/lib/shared", + "javascript-state-machine": "^3.1.0" + } + }, + "@internal/randomphrase": { + "version": "file:lib/randomphrase" + }, + "@internal/requests": { + "version": "file:lib/model/lib/requests" + }, + "@internal/router": { + "version": "file:lib/router" + }, + "@internal/shared": { + "version": "file:lib/model/lib/shared" + }, + "@internal/validate": { + "version": "file:lib/validate", + "requires": { + "ajv": "^6.7.0", + "json-schema-ref-parser": "^6.0.3", + "openapi-jsonschema-parameters": "^1.1.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", + "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^26.6.2", + "jest-util": "^26.6.2", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/core": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", + "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/reporters": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.6.2", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-resolve-dependencies": "^26.6.3", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "jest-watcher": "^26.6.2", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/environment": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", + "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/fake-timers": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", + "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/globals": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", + "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/types": "^26.6.2", + "expect": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/reporters": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", + "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^7.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/source-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", + "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", + "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/test-sequencer": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", + "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3" + } + }, + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@korzio/djv-draft-04": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@korzio/djv-draft-04/-/djv-draft-04-2.0.1.tgz", + "integrity": "sha512-MeTVcNsfCIYxK6T7jW1sroC7dBAb4IfLmQe6RoCqlxHN5NFkzNpgdnBPR+/0D2wJDUJHM9s9NQv+ouhxKjvUjg==", + "dev": true, + "optional": true + }, + "@mojaloop/sdk-standard-components": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@mojaloop/sdk-standard-components/-/sdk-standard-components-13.1.0.tgz", + "integrity": "sha512-qbXAyL99oHDzy/S7SIIa015/HJEAyhlBM86Qew//1dcMVfZZU/f/ww2aDCqH9p1T4EseGGZKyvP9Qgb8RCt42g==", + "requires": { + "base64url": "3.0.1", + "fast-safe-stringify": "^2.0.7", + "ilp-packet": "2.2.0", + "jsonwebtoken": "8.5.1", + "jws": "4.0.0" + } + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@types/babel__core": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", + "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz", + "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/formidable": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.0.32.tgz", + "integrity": "sha512-jOAB5+GFW+C+2xdvUcpd/CnYg2rD5xCyagJLBJU+9PB4a/DKmsAqS9yZI3j/Q9zwvM7ztPHaAIH1ijzp4cezdQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/graceful-fs": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", + "integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz", + "integrity": "sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw==", + "dev": true, + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + } + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/node": { + "version": "14.14.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.17.tgz", + "integrity": "sha512-G0lD1/7qD60TJ/mZmhog76k7NcpLWkPVGgzkRy3CTlnFu4LUQh5v2Wa661z6vnXmD8EQrnALUyf0VRtrACYztw==" + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/prettier": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.6.tgz", + "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", + "dev": true + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" + }, + "@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-includes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz", + "integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "get-intrinsic": "^1.0.1", + "is-string": "^1.0.5" + } + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "array.prototype.flat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "audit-resolve-core": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/audit-resolve-core/-/audit-resolve-core-1.1.8.tgz", + "integrity": "sha512-F3IWaxu1Xw4OokmtG9hkmsKoJt8DQS7RZvot52zXHsANKvzFRMKVNTP1DAz1ztlRGmJx1XV16PcE+6m35bYoTA==", + "dev": true, + "requires": { + "concat-stream": "^1.6.2", + "debug": "^4.1.1", + "djv": "^2.1.2", + "spawn-shell": "^2.1.0", + "yargs-parser": "^18.1.3" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "dev": true, + "requires": { + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bignumber.js": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-5.0.0.tgz", + "integrity": "sha512-KWTu6ZMVk9sxlDJQh2YH1UOnfDP8O8TpxUxgQG/vKASoSnEjK9aVuOueFaPcQEYQ5fyNXNTOYwYw3099RYebWg==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cjs-module-lexer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", + "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-bluebird": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/co-bluebird/-/co-bluebird-1.1.0.tgz", + "integrity": "sha1-yLnzqTIKftMJh9zKGlw8/1llXHw=", + "requires": { + "bluebird": "^2.10.0", + "co-use": "^1.1.0" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "co-body": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", + "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, + "co-use": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/co-use/-/co-use-1.1.0.tgz", + "integrity": "sha1-xrs83xDLc17Kqdru2kbXJclKTmI=" + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "confusing-browser-globals": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", + "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "default-shell": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/default-shell/-/default-shell-1.0.1.tgz", + "integrity": "sha1-dSMEvdxhdPSespy5iP7qC4gTyLw=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "djv": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/djv/-/djv-2.1.4.tgz", + "integrity": "sha512-giDn+BVbtLlwtkvtcsZjbjzpALHB77skiv3FIu6Wp8b5j8BunDcVJYH0cGUaexp6s0Sb7IkquXXjsLBJhXwQpA==", + "dev": true, + "requires": { + "@korzio/djv-draft-04": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emittery": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", + "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "env-var": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/env-var/-/env-var-6.3.0.tgz", + "integrity": "sha512-gaNzDZuVaJQJlP2SigAZLu/FieZN5MzdN7lgHNehESwlRanHwGQ/WUtJ7q//dhrj3aGBZM45yEaKOuvSJaf4mA==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz", + "integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extensible-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extensible-error/-/extensible-error-1.0.2.tgz", + "integrity": "sha512-kXU1FiTsGT8PyMKtFM074RK/VBpzwuQJicAHqBpsPDeTXBQiSALPjkjKXlyKdG/GP6lR7bBaEkq8qdoO2geu9g==" + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "format-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", + "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==" + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.2.1.tgz", + "integrity": "sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ilp-packet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ilp-packet/-/ilp-packet-2.2.0.tgz", + "integrity": "sha1-qHJcwmMxxuLGU1OKEGPVUBQwXjE=", + "requires": { + "bignumber.js": "^5.0.0", + "extensible-error": "^1.0.2", + "long": "^3.2.0", + "oer-utils": "^1.3.2" + } + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true, + "optional": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", + "integrity": "sha1-wUwhBX7TbjKNuANHlmxpP4hjifM=" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-generator-function": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.8.tgz", + "integrity": "sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "javascript-state-machine": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/javascript-state-machine/-/javascript-state-machine-3.1.0.tgz", + "integrity": "sha512-BwhYxQ1OPenBPXC735RgfB+ZUG8H3kjsx8hrYTgWnoy6TPipEy4fiicyhT2lxRKAXq9pG7CfFT8a2HLr6Hmwxg==" + }, + "jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", + "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "import-local": "^3.0.2", + "jest-cli": "^26.6.3" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-cli": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "prompts": "^2.0.1", + "yargs": "^15.4.1" + } + } + } + }, + "jest-changed-files": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", + "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "execa": "^4.0.0", + "throat": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + } + } + }, + "jest-config": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", + "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^26.6.3", + "@jest/types": "^26.6.2", + "babel-jest": "^26.6.3", + "chalk": "^4.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^26.6.2", + "jest-environment-node": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.6.3", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-docblock": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-junit": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-10.0.0.tgz", + "integrity": "sha512-dbOVRyxHprdSpwSAR9/YshLwmnwf+RSl5hf0kCGlhAcEeZY9aRqo4oNmaT0tLC16Zy9D0zekDjWkjHGjXlglaQ==", + "dev": true, + "requires": { + "jest-validate": "^24.9.0", + "mkdirp": "^0.5.1", + "strip-ansi": "^5.2.0", + "uuid": "^3.3.3", + "xml": "^1.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true + }, + "jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "jest-leak-detector": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", + "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "dev": true, + "requires": { + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-mock": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", + "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-resolve": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", + "slash": "^3.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", + "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-runner": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", + "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.7.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.6.2", + "jest-leak-detector": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-runtime": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", + "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/globals": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^0.6.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.4.1" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", + "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.0.0", + "chalk": "^4.0.0", + "expect": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "natural-compare": "^1.4.0", + "pretty-format": "^26.6.2", + "semver": "^7.3.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-validate": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", + "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "camelcase": "^6.0.0", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "leven": "^3.1.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", + "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^26.6.2", + "string-length": "^4.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz", + "integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "acorn": "^7.1.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.2.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.0", + "domexception": "^2.0.1", + "escodegen": "^1.14.1", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "saxes": "^5.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0", + "ws": "^7.2.3", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-ref-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz", + "integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==", + "requires": { + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.12.1", + "ono": "^4.0.11" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonlines": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz", + "integrity": "sha1-T80kbcXQ44aRkHxEqwAveC0dlMw=", + "dev": true + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + } + } + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "koa": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", + "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + } + }, + "koa-body": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/koa-body/-/koa-body-4.2.0.tgz", + "integrity": "sha512-wdGu7b9amk4Fnk/ytH8GuWwfs4fsB5iNkY8kZPpgQVb04QZSv85T0M8reb+cJmvLE8cjPYvBzRikD3s6qz8OoA==", + "requires": { + "@types/formidable": "^1.0.31", + "co-body": "^5.1.1", + "formidable": "^1.1.1" + }, + "dependencies": { + "co-body": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-5.2.0.tgz", + "integrity": "sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.4.0", + "raw-body": "^2.2.0", + "type-is": "^1.6.14" + } + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa2-oauth-server": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa2-oauth-server/-/koa2-oauth-server-1.0.0.tgz", + "integrity": "sha512-MsLHCOhtOEeLEQsyko2IeGwSwhzXf8DXDYohJlMaPeb12J+xYq0cp9t9r9aQuKpO+KeEtuxDFVcT24fG1Rbcdg==", + "requires": { + "bluebird": "^3.0.5", + "koa": "^2.3.0", + "oauth2-server": "git://github.com/oauthjs/node-oauth2-server.git#dev" + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz", + "integrity": "sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nock": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-12.0.3.tgz", + "integrity": "sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.13", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", + "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" + }, + "dependencies": { + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-audit-resolver": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/npm-audit-resolver/-/npm-audit-resolver-2.2.0.tgz", + "integrity": "sha512-nBhxrc0Y34vIFl38G42PkWSBEbOAL3Gg6aRxm1hYzM4Vm+Rv0ozALj2LixdeytkUC2OGWP4QqCF0fKAb14NnPQ==", + "dev": true, + "requires": { + "audit-resolve-core": "^1.1.7", + "chalk": "^2.4.2", + "djv": "^2.1.2", + "jsonlines": "^0.1.1", + "read": "^1.0.7", + "spawn-shell": "^2.1.0", + "yargs-parser": "^13.1.1", + "yargs-unparser": "^1.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + }, + "dependencies": { + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + } + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "oauth2-server": { + "version": "git://github.com/oauthjs/node-oauth2-server.git#45b508afa2f7b6272df54d324b95bc3f7528cdbb", + "from": "git://github.com/oauthjs/node-oauth2-server.git#dev", + "requires": { + "basic-auth": "^2.0.0", + "bluebird": "^3.5.1", + "lodash": "^4.17.10", + "promisify-any": "^2.0.1", + "statuses": "^1.5.0", + "type-is": "^1.6.16" + } + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz", + "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + } + }, + "oer-utils": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/oer-utils/-/oer-utils-1.3.4.tgz", + "integrity": "sha1-sqmtvJK8GRVaKgDwRWg9Hm1KyCM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "ono": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", + "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==", + "requires": { + "format-util": "^1.0.3" + } + }, + "openapi-jsonschema-parameters": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-1.2.0.tgz", + "integrity": "sha512-i2vBBFiRbOwYSvt5OG9hayJ7WUe/nl9Y151Ki1QtHb8M0zdYs2wkDhywVJnapq4/gPlrD1vmSVsYDrAjcBRJTQ==", + "requires": { + "openapi-types": "1.3.5" + } + }, + "openapi-response-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-4.0.0.tgz", + "integrity": "sha512-bIG8bpHT/vE+Dtz4aVyfQnweXtUdvxvJf5/D6Uu98UGf3T42Ez940ctwnlmDCQxTPqdu0yLFbMoiNf/A3jYCIg==", + "dev": true, + "requires": { + "ajv": "^6.5.4", + "openapi-types": "1.3.5" + } + }, + "openapi-types": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", + "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-each-series": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promisify-any": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promisify-any/-/promisify-any-2.0.1.tgz", + "integrity": "sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU=", + "requires": { + "bluebird": "^2.10.0", + "co-bluebird": "^1.1.0", + "is-generator": "^1.0.2" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "prompts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.6.0" + } + }, + "redis-commands": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", + "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" + }, + "redis-mock": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.49.0.tgz", + "integrity": "sha512-FW/cLZvF1PAVN/PYIwXf1vQRoJCyYCwUMtq8BXRwrvb9LNNAT4RKXM02Qlt6qSkC/98hmHlU2EGoQoxVy3E2lA==", + "dev": true + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spawn-shell": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spawn-shell/-/spawn-shell-2.1.0.tgz", + "integrity": "sha512-mjlYAQbZPHd4YsoHEe+i0Xbp9sJefMKN09JPp80TqrjC5NSuo+y1RG3NBireJlzl1dDV2NIkIfgS6coXtyqN/A==", + "dev": true, + "requires": { + "default-shell": "^1.0.1", + "merge-options": "~1.0.1", + "npm-run-path": "^2.0.2" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "string-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz", + "integrity": "sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "table": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.4.tgz", + "integrity": "sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "uuidv4": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.6.tgz", + "integrity": "sha512-vFyL4jugB/ln1ux1gXLlBMBv424Dn86EaBMoqUH1K6XI3XuriaWLeRUzH4iWwPu+BOJiw4hc4TjvrPmk+H+ZBQ==", + "requires": { + "@types/uuid": "8.3.0", + "uuid": "8.3.2" + } + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "v8-to-istanbul": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz", + "integrity": "sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.4.tgz", + "integrity": "sha512-QxEx9+qEr7jwVM4ngnk95+sKZ5QXm5gx0cL97LDby0SiC8HHoUK0LPBg475JwQcRCqIVfMD8SubCWp1dEgKuwQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "decamelize": "^1.2.0", + "flat": "^5.0.2", + "is-plain-obj": "^1.1.0", + "yargs": "^14.2.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + } + } +} diff --git a/src/package.json b/src/package.json index e00f788c3..fbe0b62bf 100644 --- a/src/package.json +++ b/src/package.json @@ -1,15 +1,24 @@ { "name": "@mojaloop/sdk-scheme-adapter", - "version": "10.1.5", + "version": "11.10.1", "description": "An adapter for connecting to Mojaloop API enabled switches.", "main": "index.js", "scripts": { + "audit:resolve": "SHELL=sh resolve-audit --production", + "audit:check": "SHELL=sh check-audit --production", "lint": "eslint .", "lint:fix": "eslint . --fix", - "test": "jest --ci --reporters=default --reporters=jest-junit --env=node test/unit", + "test": "jest --ci --reporters=default --reporters=jest-junit --env=node test/unit/", "test:int": "jest --ci --reporters=default --reporters=jest-junit --env=node test/integration --forceExit" }, "author": "Matt Kingston, James Bush, ModusBox Inc.", + "contributors": [ + "Kamuela Franco ", + "Steven Oderayi ", + "Valentin Genev ", + "Shashikant Hirugade ", + "Paweł Marzec " + ], "license": "Apache-2.0", "licenses": [ { @@ -19,33 +28,35 @@ ], "dependencies": { "@internal/cache": "file:lib/cache", - "@internal/log": "file:lib/log", + "@internal/check": "file:lib/check", "@internal/model": "file:lib/model", "@internal/randomphrase": "file:lib/randomphrase", "@internal/requests": "file:lib/model/lib/requests", "@internal/router": "file:lib/router", "@internal/shared": "file:lib/model/lib/shared", "@internal/validate": "file:lib/validate", - "@mojaloop/sdk-standard-components": "^10.2.2", - "ajv": "^6.12.2", - "co-body": "^6.0.0", + "@mojaloop/sdk-standard-components": "13.1.0", + "ajv": "^6.12.6", + "co-body": "^6.1.0", "dotenv": "^8.2.0", "env-var": "^6.1.1", - "js-yaml": "^3.13.1", + "js-yaml": "^3.14.1", "koa": "^2.11.0", "koa-body": "^4.1.1", "koa2-oauth-server": "^1.0.0", "node-fetch": "^2.6.0", - "uuidv4": "^6.0.8" + "uuidv4": "^6.2.6", + "ws": "^7.4.1" }, "devDependencies": { "@types/jest": "^25.2.1", - "eslint": "^7.0.0", - "eslint-config-airbnb-base": "^14.1.0", - "eslint-plugin-import": "^2.20.2", - "jest": "^26.0.1", + "eslint": "^7.15.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.22.1", + "jest": "^26.6.3", "jest-junit": "^10.0.0", "nock": "^12.0.3", + "npm-audit-resolver": "2.2.0", "openapi-response-validator": "^4.0.0", "redis-mock": "^0.49.0", "supertest": "^4.0.2" diff --git a/src/test/__mocks__/@internal/requests.js b/src/test/__mocks__/@internal/requests.js index 64dad53ea..5384281d8 100644 --- a/src/test/__mocks__/@internal/requests.js +++ b/src/test/__mocks__/@internal/requests.js @@ -23,19 +23,32 @@ class MockBackendRequests extends BackendRequests { super(...args); MockBackendRequests.__instance = this; this.getOTP = MockBackendRequests.__getOTP; + this.getSignedChallenge = MockBackendRequests.__getSignedChallenge; this.getParties = MockBackendRequests.__getParties; this.postTransactionRequests = MockBackendRequests.__postTransactionRequests; this.postQuoteRequests = MockBackendRequests.__postQuoteRequests; this.getTransfers = MockBackendRequests.__getTransfers; this.postTransfers = MockBackendRequests.__postTransfers; + this.getBulkQuotes = MockBackendRequests.__getBulkQuotes; + this.postBulkQuotes = MockBackendRequests.__postBulkQuotes; + this.getBulkTransfers = MockBackendRequests.__getBulkTransfers; + this.postBulkTransfers = MockBackendRequests.__postBulkTransfers; + this.putTransfersNotification = MockBackendRequests.__putTransfersNotification; } } MockBackendRequests.__getParties = jest.fn(() => Promise.resolve({body: {}})); MockBackendRequests.__getOTP = jest.fn(() => Promise.resolve({body: {}})); +MockBackendRequests.__getSignedChallenge = jest.fn(() => Promise.resolve({body: {}})); MockBackendRequests.__postTransactionRequests = jest.fn(() => Promise.resolve({body: {}})); MockBackendRequests.__postQuoteRequests = jest.fn(() => Promise.resolve({body: {}})); MockBackendRequests.__getTransfers = jest.fn(() => Promise.resolve({body: {}})); MockBackendRequests.__postTransfers = jest.fn(() => Promise.resolve({body: {}})); +MockBackendRequests.__getBulkQuotes = jest.fn(() => Promise.resolve({body: {}})); +MockBackendRequests.__postBulkQuotes = jest.fn(() => Promise.resolve({body: {}})); +MockBackendRequests.__getBulkTransfers = jest.fn(() => Promise.resolve({body: {}})); +MockBackendRequests.__postBulkTransfers = jest.fn(() => Promise.resolve({body: {}})); +MockBackendRequests.__putTransfersNotification = jest.fn(() => Promise.resolve({body: {}})); + class HTTPResponseError extends Error { diff --git a/src/test/__mocks__/@mojaloop/sdk-standard-components.js b/src/test/__mocks__/@mojaloop/sdk-standard-components.js index f21135e55..423051730 100644 --- a/src/test/__mocks__/@mojaloop/sdk-standard-components.js +++ b/src/test/__mocks__/@mojaloop/sdk-standard-components.js @@ -10,8 +10,9 @@ 'use strict'; +const assert = require('assert').strict; const util = require('util'); -const { MojaloopRequests, Errors, WSO2Auth, Jws } = jest.requireActual('@mojaloop/sdk-standard-components'); +const { MojaloopRequests, ThirdpartyRequests, Errors, WSO2Auth, Jws, Logger } = jest.requireActual('@mojaloop/sdk-standard-components'); class MockMojaloopRequests extends MojaloopRequests { @@ -26,11 +27,21 @@ class MockMojaloopRequests extends MojaloopRequests { this.putQuotesError = MockMojaloopRequests.__putQuotesError; this.getAuthorizations = MockMojaloopRequests.__getAuthorizations; this.putAuthorizations = MockMojaloopRequests.__putAuthorizations; + this.postAuthorizations = MockMojaloopRequests.__postAuthorizations; + this.getTransfers = MockMojaloopRequests.__getTransfers; this.putTransactionRequests = MockMojaloopRequests.__putTransactionRequests; this.postTransfers = MockMojaloopRequests.__postTransfers; this.putTransfers = MockMojaloopRequests.__putTransfers; this.putTransfersError = MockMojaloopRequests.__putTransfersError; + this.getBulkQuotes = MockMojaloopRequests.__getBulkQuotes; + this.postBulkQuotes = MockMojaloopRequests.__postBulkQuotes; + this.putBulkQuotes = MockMojaloopRequests.__putBulkQuotes; + this.putBulkQuotesError = MockMojaloopRequests.__putBulkQuotesError; + this.getBulkTransfers = MockMojaloopRequests.__getBulkTransfers; + this.postBulkTransfers = MockMojaloopRequests.__postBulkTransfers; + this.putBulkTransfers = MockMojaloopRequests.__putBulkTransfers; + this.putBulkTransfersError = MockMojaloopRequests.__putBulkTransfersError; } } MockMojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve()); @@ -41,44 +52,75 @@ MockMojaloopRequests.__putQuotes = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__putQuotesError = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__getAuthorizations = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__putAuthorizations = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__postAuthorizations = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__getTransfers = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__putTransactionRequests = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__postTransfers = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__putTransfers = jest.fn(() => Promise.resolve()); MockMojaloopRequests.__putTransfersError = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__getBulkQuotes = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__postBulkQuotes = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__putBulkQuotes = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__putBulkQuotesError = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__getBulkTransfers = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__postBulkTransfers = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__putBulkTransfers = jest.fn(() => Promise.resolve()); +MockMojaloopRequests.__putBulkTransfersError = jest.fn(() => Promise.resolve()); + + +class MockThirdpartyRequests extends ThirdpartyRequests { + constructor(...args) { + super(...args); + MockThirdpartyRequests.__instance = this; + this.postAuthorizations = MockMojaloopRequests.__postAuthorizations; + this.getThirdpartyRequestsTransactions = MockThirdpartyRequests.__getThirdpartyRequestsTransactions; + this.postThirdpartyRequestsTransactions = MockThirdpartyRequests.__postThirdpartyRequestsTransactions; + } +} +MockThirdpartyRequests.__getThirdpartyRequestsTransactions = jest.fn(() => Promise.resolve()); +MockThirdpartyRequests.__postThirdpartyRequestsTransactions = jest.fn(() => Promise.resolve()); class MockIlp { constructor(config) { - console.log('MockIlp constructed'); + assert(config.logger, 'Must supply a logger to Ilp constructor'); + this.logger = config.logger; + this.logger.log('MockIlp constructed'); this.config = config; } calculateFulfil(ilpPacket) { - console.log(`Mock ILP not calculating fulfil from ilp packet ${ilpPacket}`); + this.logger.log(`Mock ILP not calculating fulfil from ilp packet ${ilpPacket}`); return 'mockGeneratedFulfilment'; } calculateConditionFromFulfil(fulfil) { - console.log(`Mock ILP not calculating condition from fulfil ${fulfil}`); + this.logger.log(`Mock ILP not calculating condition from fulfil ${fulfil}`); return 'mockGeneratedCondition'; } validateFulfil(fulfil, condition) { - console.log(`Mock ILP not checking fulfil ${fulfil} against condition ${condition}`); + this.logger.log(`Mock ILP not checking fulfil ${fulfil} against condition ${condition}`); return true; } getResponseIlp(...args) { - console.log(`MockIlp.getResponseIlp called with args: ${util.inspect(args)}`); + this.logger.log(`MockIlp.getResponseIlp called with args: ${util.inspect(args)}`); return MockIlp.__response; } getQuoteResponseIlp(...args) { - console.log(`MockIlp.getQuoteResponseIlp called with args: ${util.inspect(args)}`); + this.logger.log(`MockIlp.getQuoteResponseIlp called with args: ${util.inspect(args)}`); return this.getResponseIlp(...args); } + + + getTransactionObject(...args) { + this.logger.log(`MockIlp.getTrasnactionObject called with args: ${util.inspect(args)}`); + + return MockIlp.__transactionObject; + } } MockIlp.__response = { fulfilment: 'mockGeneratedFulfilment', @@ -86,6 +128,10 @@ MockIlp.__response = { condition: 'mockGeneratedCondition' }; +MockIlp.__transactionObject = { + transactionId: 'mockTransactionId' +}; + class MockJwsValidator extends Jws.validator { constructor(config) { @@ -99,14 +145,16 @@ MockJwsValidator.__validate = jest.fn(() => true); class MockJwsSigner { constructor(config) { + assert(config.logger, 'Must supply a logger to JWS signer constructor'); this.config = config; - console.log(`MockJwsSigner constructed with config: ${util.inspect(config)}`); + config.logger.log(`MockJwsSigner constructed with config: ${util.inspect(config)}`); } } module.exports = { MojaloopRequests: MockMojaloopRequests, + ThirdpartyRequests: MockThirdpartyRequests, Ilp: MockIlp, Jws: { validator: MockJwsValidator, @@ -114,4 +162,5 @@ module.exports = { }, Errors, WSO2Auth, + Logger, }; diff --git a/src/test/config/integration.env b/src/test/config/integration.env index 07af45edf..c17f26730 100644 --- a/src/test/config/integration.env +++ b/src/test/config/integration.env @@ -49,11 +49,14 @@ CACHE_SHOULD_EXPIRE=false CACHE_EXPIRY_SECONDS=3600 # SWITCH ENDPOINT -# The option 'PEER_ENDPOINT' has no effect if the remaining three options 'ALS_ENDPOINT', 'QUOTES_ENDPOINT', 'TRANSFERS_ENDPOINT' are specified. +# The option 'PEER_ENDPOINT' has no effect if the remaining options 'ALS_ENDPOINT', 'QUOTES_ENDPOINT', +# 'BULK_QUOTES_ENDPOINT', 'TRANSFERS_ENDPOINT', 'BULK_TRANSFERS_ENDPOINT', 'TRANSACTION_REQUESTS_ENDPOINT' are specified. PEER_ENDPOINT=172.17.0.3:4000 #ALS_ENDPOINT=account-lookup-service.local #QUOTES_ENDPOINT=quoting-service.local #TRANSFERS_ENDPOINT=ml-api-adapter.local +#BULK_TRANSFERS_ENDPOINT=bulk-api-adapter.local +#TRANSACTION_REQUESTS_ENDPOINT=transaction-requests-service.local # BACKEND ENDPOINT BACKEND_ENDPOINT=172.17.0.5:4000 diff --git a/src/test/integration/lib/cache.test.js b/src/test/integration/lib/cache.test.js index 6b3d31729..d213865f6 100644 --- a/src/test/integration/lib/cache.test.js +++ b/src/test/integration/lib/cache.test.js @@ -33,7 +33,7 @@ jest.dontMock('redis'); const Cache = require('@internal/cache'); -const { Logger } = require('@internal/log'); +const { Logger } = require('@mojaloop/sdk-standard-components'); const defaultCacheConfig = { host: 'redis', @@ -42,13 +42,11 @@ const defaultCacheConfig = { }; const createCache = async (config) => { - const transports = []; - config.logger = new Logger({ + config.logger = new Logger.Logger({ context: { app: 'mojaloop-sdk-inboundCache' }, - space: 4, - transports + stringify: Logger.buildStringify({ space: 4 }), }); const cache = new Cache(config); await cache.connect(); @@ -66,8 +64,8 @@ describe('Cache', () => { // Act await cache.set('keyA', JSON.stringify(value)); const result = await cache.get('keyA'); - + // Assert expect(result).toStrictEqual(value); }); -}); \ No newline at end of file +}); diff --git a/src/test/unit/InboundServer.test.js b/src/test/unit/InboundServer.test.js index 613245a0d..9d419e108 100644 --- a/src/test/unit/InboundServer.test.js +++ b/src/test/unit/InboundServer.test.js @@ -22,7 +22,8 @@ jest.mock('@internal/cache'); jest.mock('@mojaloop/sdk-standard-components'); jest.mock('@internal/requests'); -const { Jws } = require('@mojaloop/sdk-standard-components'); +const Cache = require('@internal/cache'); +const { Jws, Logger } = require('@mojaloop/sdk-standard-components'); const path = require('path'); const fs = require('fs'); const os = require('os'); @@ -43,10 +44,11 @@ describe('Inbound Server', () => { async function testPartiesJwsValidation(validateInboundJws, validateInboundPutPartiesJws, expectedValidationCalls) { serverConfig.validateInboundJws = validateInboundJws; serverConfig.validateInboundPutPartiesJws = validateInboundPutPartiesJws; - const svr = new InboundServer(serverConfig); - const req = supertest(await svr.setupApi()); + const logger = new Logger.Logger({ stringify: () => '' }); + const cache = new Cache({ ...serverConfig.cacheConfig, logger: logger.push({ component: 'cache' }) }); + const svr = new InboundServer(serverConfig, logger, cache); await svr.start(); - await req + await supertest(svr._server) .put('/parties/MSISDN/123456789') .send(putPartiesBody) .set(commonHttpHeaders) @@ -79,10 +81,11 @@ describe('Inbound Server', () => { async function testQuotesJwsValidation(validateInboundJws, validateInboundPutPartiesJws, expectedValidationCalls) { serverConfig.validateInboundJws = validateInboundJws; serverConfig.validateInboundPutPartiesJws = validateInboundPutPartiesJws; - const svr = new InboundServer(serverConfig); - const req = supertest(await svr.setupApi()); + const logger = new Logger.Logger({ stringify: () => '' }); + const cache = new Cache({ ...serverConfig.cacheConfig, logger: logger.push({ component: 'cache' }) }); + const svr = new InboundServer(serverConfig, logger, cache); await svr.start(); - await req + await supertest(svr._server) .post('/quotes') .send(postQuotesBody) .set(commonHttpHeaders) @@ -110,10 +113,11 @@ describe('Inbound Server', () => { async function testParticipantsJwsValidation(validateInboundJws, validateInboundPutPartiesJws, expectedValidationCalls) { serverConfig.validateInboundJws = validateInboundJws; serverConfig.validateInboundPutPartiesJws = validateInboundPutPartiesJws; - const svr = new InboundServer(serverConfig); - const req = supertest(await svr.setupApi()); + const logger = new Logger.Logger({ stringify: () => '' }); + const cache = new Cache({ ...serverConfig.cacheConfig, logger: logger.push({ component: 'cache' }) }); + const svr = new InboundServer(serverConfig, logger, cache); await svr.start(); - await req + await supertest(svr._server) .put('/participants/00000000-0000-1000-a000-000000000002') .send(putParticipantsBody) .set(commonHttpHeaders) @@ -153,9 +157,11 @@ describe('Inbound Server', () => { }); async function testTlsServer(enableTls) { - defConfig.tls.inbound.mutualTLS.enabled = enableTls; - const server = new InboundServer(defConfig); - await server.setupApi(); + defConfig.inbound.tls.mutualTLS.enabled = enableTls; + const logger = new Logger.Logger({ stringify: () => '' }); + const cache = new Cache({ ...defConfig.cacheConfig, logger: logger.push({ component: 'cache' }) }); + const server = new InboundServer(defConfig, logger, cache); + await server.start(); if (enableTls) { expect(httpsServerSpy).toHaveBeenCalled(); expect(httpServerSpy).not.toHaveBeenCalled(); @@ -163,6 +169,7 @@ describe('Inbound Server', () => { expect(httpsServerSpy).not.toHaveBeenCalled(); expect(httpServerSpy).toHaveBeenCalled(); } + await server.stop(); } test('Inbound server should use HTTPS if inbound mTLS enabled', () => @@ -184,8 +191,9 @@ describe('Inbound Server', () => { const mockFilePath = path.join(keysDir, 'mojaloop-sdk.pem'); fs.writeFileSync(mockFilePath, 'foo-key'); serverConfig.jwsVerificationKeysDirectory = keysDir; - svr = new InboundServer(serverConfig); - await svr.setupApi(); + const logger = new Logger.Logger({ stringify: () => '' }); + const cache = new Cache({ ...serverConfig.cacheConfig, logger: logger.push({ component: 'cache' }) }); + svr = new InboundServer(serverConfig, logger, cache); await svr.start(); }); diff --git a/src/test/unit/TestServer.test.js b/src/test/unit/TestServer.test.js index 72386df57..eec764629 100644 --- a/src/test/unit/TestServer.test.js +++ b/src/test/unit/TestServer.test.js @@ -17,8 +17,10 @@ const putPartiesBody = require('./data/putPartiesBody'); const postQuotesBody = require('./data/postQuotesBody'); const putParticipantsBody = require('./data/putParticipantsBody'); const commonHttpHeaders = require('./data/commonHttpHeaders'); +const WebSocket = require('ws'); +const { Logger } = require('@mojaloop/sdk-standard-components'); -const cache = require('@internal/cache'); +const Cache = require('@internal/cache'); jest.mock('@internal/cache'); jest.mock('@mojaloop/sdk-standard-components'); jest.mock('@internal/requests'); @@ -26,40 +28,67 @@ jest.mock('@internal/requests'); const InboundServer = require('../../InboundServer'); const TestServer = require('../../TestServer'); +const createWsClient = async (port, path) => { + const result = new WebSocket(`ws://127.0.0.1:${port}${path}`); + await new Promise((resolve, reject) => { + result.on('open', resolve); + result.on('error', reject); + }); + return result; +}; + describe('Test Server', () => { - let testServer, inboundServer, inboundReq, testReq, serverConfig, inboundCache, testCache; + let testServer, inboundServer, inboundReq, testReq, serverConfig, wsClients, testServerPort, + logger, cache; beforeEach(async () => { - cache.mockClear(); + Cache.mockClear(); + + logger = new Logger.Logger({ stringify: () => '' }); serverConfig = { ...JSON.parse(JSON.stringify(defaultConfig)), enableTestFeatures: true, }; + cache = new Cache({ ...serverConfig.cacheConfig, logger: logger.push({ component: 'cache' }), enableTestFeatures: true }); - testServer = new TestServer(serverConfig); - testReq = supertest(await testServer.setupApi()); + testServer = new TestServer({ logger, cache }); await testServer.start(); - testCache = cache.mock.instances[0]; + testServerPort = testServer._server.address().port; - inboundServer = new InboundServer(serverConfig); - inboundReq = supertest(await inboundServer.setupApi()); + expect(testServer._server.listening).toBe(true); + testReq = supertest.agent(testServer._server); + + inboundServer = new InboundServer(serverConfig, logger, cache); await inboundServer.start(); - inboundCache = cache.mock.instances[1]; + inboundReq = supertest(inboundServer._server); - expect(cache).toHaveBeenCalledTimes(2); + wsClients = { + root: await createWsClient(testServerPort, '/'), + callbacks: await createWsClient(testServerPort, '/callbacks'), + requests: await createWsClient(testServerPort, '/requests'), + }; + + expect(Object.values(wsClients).every((cli) => cli.readyState === WebSocket.OPEN)).toBe(true); + expect(testServer._wsapi._wsClients.size).toBeGreaterThan(0); + + expect(Cache).toHaveBeenCalledTimes(1); }); afterEach(async () => { + await Promise.all(Object.values(wsClients).map((cli) => { + cli.close(); + return new Promise((resolve) => cli.on('close', resolve)); + })); await testServer.stop(); await inboundServer.stop(); }); // TODO: check this happens correctly with top-level server if possible? test('Inbound server and Test server construct cache with same parameters when provided same config', async () => { - expect(cache).toHaveBeenCalledTimes(2); - const testArgs = { ...cache.mock.calls[0][0], logger: expect.anything() }; - expect(cache).toHaveBeenNthCalledWith(2, testArgs); + expect(Cache).toHaveBeenCalledTimes(1); + const testArgs = { ...Cache.mock.calls[0][0], logger: expect.anything() }; + expect(Cache).toHaveBeenNthCalledWith(1, testArgs); }); test('Health check', async () => { @@ -81,7 +110,7 @@ describe('Test Server', () => { await testReq.get(`/callbacks/${MSISDN}`); - expect(inboundCache.set.mock.calls[0][0]).toEqual(testCache.get.mock.calls[0][0]); + expect(cache.set.mock.calls[0][0]).toEqual(cache.get.mock.calls[0][0]); }); test('POST /quotes requests cache get and set use same value', async () => { @@ -95,7 +124,7 @@ describe('Test Server', () => { await testReq.get(`/requests/${postQuotesBody.quoteId}`); - expect(inboundCache.set.mock.calls[0][0]).toEqual(testCache.get.mock.calls[0][0]); + expect(cache.set.mock.calls[0][0]).toEqual(cache.get.mock.calls[0][0]); }); test('PUT /participants callbacks cache get and set use same value', async () => { @@ -111,6 +140,248 @@ describe('Test Server', () => { await testReq.get(`/callbacks/${participantId}`); - expect(inboundCache.set.mock.calls[0][0]).toEqual(testCache.get.mock.calls[0][0]); + expect(cache.set.mock.calls[0][0]).toEqual(cache.get.mock.calls[0][0]); + }); + + test('Subscribes to the keyevent set notification', async () => { + expect(testServer._wsapi._cache.subscribe).toBeCalledTimes(1); + expect(testServer._wsapi._cache.subscribe).toHaveBeenCalledWith( + testServer._wsapi._cache.EVENT_SET, + expect.any(Function), + ); + }); + + test('WebSocket /callbacks and / endpoint triggers send to client when callback received to inbound server', async () => { + const participantId = '00000000-0000-1000-a000-000000000002'; + + const headers = { + ...commonHttpHeaders, + 'fspiop-http-method': 'PUT', + 'fspiop-uri': `/participants/${participantId}`, + 'date': new Date().toISOString(), + }; + + const putParticipantWsClient = await createWsClient( + testServerPort, + `/callbacks/${participantId}` + ); + + const putParticipantEndpointMessageReceived = new Promise(resolve => { + putParticipantWsClient.on('message', resolve); + }); + const serverCallbackEndpointMessageReceived = new Promise(resolve => { + wsClients.callbacks.on('message', resolve); + }); + const serverRootEndpointMessageReceived = new Promise(resolve => { + wsClients.root.on('message', resolve); + }); + + // get the callback function that the test server subscribed with, and mock the cache by + // calling the callback when the inbound server sets a key in the cache. + const callback = testServer._wsapi._cache.subscribe.mock.calls[0][1]; + inboundServer._api._cache.set = jest.fn(async (key) => await callback( + inboundServer._api._cache.EVENT_SET, + key, + 1, + )); + testServer._wsapi._cache.get = jest.fn(() => ({ + data: putParticipantsBody, + headers, + })); + + await inboundReq + .put(`/participants/${participantId}`) + .send(putParticipantsBody) + .set(headers); + + expect(inboundServer._api._cache.set).toHaveBeenCalledTimes(1); + expect(inboundServer._api._cache.set).toHaveBeenCalledWith( + `${testServer._wsapi._cache.CALLBACK_PREFIX}${participantId}`, + { + data: putParticipantsBody, + headers: expect.objectContaining(headers), + } + ); + + expect(testServer._wsapi._cache.get).toHaveBeenCalledTimes(1); + expect(testServer._wsapi._cache.get).toHaveBeenCalledWith( + `${testServer._wsapi._cache.CALLBACK_PREFIX}${participantId}` + ); + + const expectedMessage = { + data: putParticipantsBody, + headers: expect.objectContaining(headers), + id: participantId + }; + + // Expect the client websockets to receive a message containing the callback headers and + // body + const callbackClientResult = JSON.parse(await serverCallbackEndpointMessageReceived); + expect(callbackClientResult).toEqual(expectedMessage); + const rootClientResult = JSON.parse(await serverRootEndpointMessageReceived); + expect(rootClientResult).toEqual(expectedMessage); + const putParticipantClientClientResult = JSON.parse(await putParticipantEndpointMessageReceived); + expect(putParticipantClientClientResult).toEqual(expectedMessage); + }); + + test('WebSocket /requests and / endpoint triggers send to client when callback received to inbound server', async () => { + const headers = { + ...commonHttpHeaders, + 'fspiop-http-method': 'POST', + 'fspiop-uri': '/quotes', + 'date': new Date().toISOString(), + }; + + const postQuoteWsClient = await createWsClient( + testServerPort, + `/requests/${postQuotesBody.quoteId}` + ); + + const postQuoteEndpointMessageReceived = new Promise(resolve => { + postQuoteWsClient.on('message', resolve); + }); + const serverRequestEndpointMessageReceived = new Promise(resolve => { + wsClients.requests.on('message', resolve); + }); + const serverRootEndpointMessageReceived = new Promise(resolve => { + wsClients.root.on('message', resolve); + }); + + // get the callback function that the test server subscribed with, and mock the cache by + // calling the callback when the inbound server sets a key in the cache. + const callback = testServer._wsapi._cache.subscribe.mock.calls[0][1]; + inboundServer._api._cache.set = jest.fn(async (key) => await callback( + inboundServer._api._cache.EVENT_SET, + key, + 1, + )); + testServer._wsapi._cache.get = jest.fn(() => ({ + data: postQuotesBody, + headers, + })); + + await inboundReq + .post('/quotes') + .send(postQuotesBody) + .set(headers); + + // Called twice for the quote request, once for the fulfilment + expect(inboundServer._api._cache.set).toHaveBeenCalledTimes(3); + expect(inboundServer._api._cache.set).toHaveBeenCalledWith( + `${testServer._wsapi._cache.REQUEST_PREFIX}${postQuotesBody.quoteId}`, + { + data: postQuotesBody, + headers: expect.objectContaining(headers), + } + ); + + expect(testServer._wsapi._cache.get).toHaveBeenCalledTimes(1); + expect(testServer._wsapi._cache.get).toHaveBeenCalledWith( + `${testServer._wsapi._cache.REQUEST_PREFIX}${postQuotesBody.quoteId}` + ); + + const expectedMessage = { + data: postQuotesBody, + headers: expect.objectContaining(headers), + id: postQuotesBody.quoteId, + }; + + // Expect the client websockets to receive a message containing the callback headers and + // body + const callbackClientResult = JSON.parse(await serverRequestEndpointMessageReceived); + expect(callbackClientResult).toEqual(expectedMessage); + const rootClientResult = JSON.parse(await serverRootEndpointMessageReceived); + expect(rootClientResult).toEqual(expectedMessage); + const postQuoteClientResult = JSON.parse(await postQuoteEndpointMessageReceived); + expect(postQuoteClientResult).toEqual(expectedMessage); + }); + + test('Websocket / endpoint receives both callbacks and requests', async () => { + const quoteRequestHeaders = { + ...commonHttpHeaders, + 'fspiop-http-method': 'POST', + 'fspiop-uri': '/quotes', + 'date': new Date().toISOString(), + }; + + const serverRootEndpointMessageReceived = new Promise(resolve => { + wsClients.root.on('message', resolve); + }); + + // get the callback function that the test server subscribed with, and mock the cache by + // calling the callback when the inbound server sets a key in the cache. + const callback = testServer._wsapi._cache.subscribe.mock.calls[0][1]; + inboundServer._api._cache.set = jest.fn(async (key) => await callback( + inboundServer._api._cache.EVENT_SET, + key, + 1, + )); + testServer._wsapi._cache.get = jest.fn(() => ({ + data: postQuotesBody, + headers: quoteRequestHeaders, + })); + + await inboundReq + .post('/quotes') + .send(postQuotesBody) + .set(quoteRequestHeaders); + + // Called twice for the quote request, once for the fulfilment + expect(inboundServer._api._cache.set).toHaveBeenCalledTimes(3); + expect(inboundServer._api._cache.set).toHaveBeenCalledWith( + `${testServer._wsapi._cache.REQUEST_PREFIX}${postQuotesBody.quoteId}`, + { + data: postQuotesBody, + headers: expect.objectContaining(quoteRequestHeaders), + } + ); + + expect(testServer._wsapi._cache.get).toHaveBeenCalledTimes(1); + expect(testServer._wsapi._cache.get).toHaveBeenCalledWith( + `${testServer._wsapi._cache.REQUEST_PREFIX}${postQuotesBody.quoteId}` + ); + + const expectedMessage = { + data: postQuotesBody, + headers: expect.objectContaining(quoteRequestHeaders), + id: postQuotesBody.quoteId, + }; + + // Expect the client websockets to receive a message containing the callback + // quoteRequestHeaders and body + const rootEndpointResult = JSON.parse(await serverRootEndpointMessageReceived); + expect(rootEndpointResult).toEqual(expectedMessage); + + const participantId = '00000000-0000-1000-a000-000000000002'; + + const putParticipantsHeaders = { + ...commonHttpHeaders, + 'fspiop-http-method': 'PUT', + 'fspiop-uri': `/participants/${participantId}`, + 'date': new Date().toISOString(), + }; + + await inboundReq + .put(`/participants/${participantId}`) + .send(putParticipantsBody) + .set(putParticipantsHeaders); + + // Called thrice for the quote request earlier in this test, another time now for the put + // participants request + expect(inboundServer._api._cache.set).toHaveBeenCalledTimes(4); + expect(inboundServer._api._cache.set.mock.calls[3]).toEqual([ + `${testServer._wsapi._cache.CALLBACK_PREFIX}${participantId}`, + { + data: putParticipantsBody, + headers: expect.objectContaining(putParticipantsHeaders), + } + ]); + + // Called once for the quote request earlier in this test, another time now for the + // participants callback + expect(testServer._wsapi._cache.get).toHaveBeenCalledTimes(2); + expect(testServer._wsapi._cache.get.mock.calls[1]).toEqual([ + `${testServer._wsapi._cache.CALLBACK_PREFIX}${participantId}` + ]); }); }); diff --git a/src/test/unit/api/accounts/utils.js b/src/test/unit/api/accounts/utils.js index 95320c1ff..4eedf835a 100644 --- a/src/test/unit/api/accounts/utils.js +++ b/src/test/unit/api/accounts/utils.js @@ -53,7 +53,6 @@ function createPostAccountsTester({ reqInbound, reqOutbound, apiSpecsOutbound }) const responseValidator = new OpenAPIResponseValidator(apiSpecsOutbound.paths['/accounts'].post); const err = responseValidator.validateResponse(responseCode, body); if (err) { - console.log(body); throw err; } await pendingRequest; diff --git a/src/test/unit/api/transfers/data/putPartiesBody.json b/src/test/unit/api/transfers/data/putPartiesBody.json index e46c03950..817843bd0 100644 --- a/src/test/unit/api/transfers/data/putPartiesBody.json +++ b/src/test/unit/api/transfers/data/putPartiesBody.json @@ -1,5 +1,19 @@ { "party": { + "accounts": { + "account": [ + { + "currency": "USD", + "description": "savings", + "address": "moja.red.8f027046-b82a5456-4fa9-838b-70210fcf8136" + }, + { + "currency": "USD", + "description": "checkings", + "address": "moja.red.8f027046-b8236345a-4fa9-838b-70210fcf8137" + } + ] + }, "partyIdInfo": { "partyIdType": "PERSONAL_ID", "partyIdentifier": "987654321", @@ -17,4 +31,4 @@ "name": "John Doe", "merchantClassificationCode": "1234" } -} +} \ No newline at end of file diff --git a/src/test/unit/api/transfers/utils.js b/src/test/unit/api/transfers/utils.js index 40b4b9ad4..416b6435e 100644 --- a/src/test/unit/api/transfers/utils.js +++ b/src/test/unit/api/transfers/utils.js @@ -1,7 +1,7 @@ const nock = require('nock'); const OpenAPIResponseValidator = require('openapi-response-validator').default; -const { Logger } = require('@internal/log'); +const { Logger } = require('@mojaloop/sdk-standard-components'); const defaultConfig = require('../../data/defaultConfig'); const postTransfersSimpleBody = require('./data/postTransfersSimpleBody'); @@ -49,11 +49,14 @@ function createGetTransfersTester({ reqInbound, reqOutbound, apiSpecsOutbound }) const res = await reqOutbound.get(`/transfers/${TRANSFER_ID}`); const {body} = res; expect(res.statusCode).toEqual(responseCode); + delete body.initiatedTimestamp; + if (body.transferState) { + delete body.transferState.initiatedTimestamp; + } expect(body).toEqual(responseBody); const responseValidator = new OpenAPIResponseValidator(apiSpecsOutbound.paths['/transfers/{transferId}'].get); const err = responseValidator.validateResponse(responseCode, body); if (err) { - console.log(body); throw err; } }; @@ -70,12 +73,7 @@ function createGetTransfersTester({ reqInbound, reqOutbound, apiSpecsOutbound }) function createPostTransfersTester( { requestValidatorInbound, reqInbound, reqOutbound, apiSpecsOutbound }) { - const logTransports = [() => {}]; - const logger = new Logger({ - context: { app: 'outbound-model-unit-tests' }, - space: 4, - transports: logTransports, - }); + const logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests' }, stringify: () => '' }); /** * @@ -159,11 +157,14 @@ function createPostTransfersTester( const res = await reqOutbound.post('/transfers').send(postTransfersSimpleBody); const {body} = res; expect(res.statusCode).toEqual(responseCode); + delete body.initiatedTimestamp; + if (body.transferState) { + delete body.transferState.initiatedTimestamp; + } expect(body).toEqual(responseBody); const responseValidator = new OpenAPIResponseValidator(apiSpecsOutbound.paths['/transfers'].post); const err = responseValidator.validateResponse(responseCode, body); if (err) { - console.log(body); throw err; } await pendingRequest; diff --git a/src/test/unit/api/utils.js b/src/test/unit/api/utils.js index b6b61ad01..c005c26ac 100644 --- a/src/test/unit/api/utils.js +++ b/src/test/unit/api/utils.js @@ -6,6 +6,8 @@ const Validate = require('@internal/validate'); const InboundServer = require('../../../InboundServer'); const OutboundServer = require('../../../OutboundServer'); +const { Logger } = require('@mojaloop/sdk-standard-components'); +const Cache = require('@internal/cache'); /** * Get OpenAPI spec and Validator for specified server @@ -37,15 +39,21 @@ const createValidators = async () => { }; const createTestServers = async (config) => { + const logger = new Logger.Logger({ stringify: () => '' }); const defConfig = JSON.parse(JSON.stringify(config)); + const cache = new Cache({ + ...defConfig.cacheConfig, + logger: logger.push({ component: 'cache' }) + }); + await cache.connect(); defConfig.requestProcessingTimeoutSeconds = 2; - const serverOutbound = new OutboundServer(defConfig); - const reqOutbound = supertest(await serverOutbound.setupApi()); + const serverOutbound = new OutboundServer(defConfig, logger, cache); await serverOutbound.start(); + const reqOutbound = supertest(serverOutbound._server); - const serverInbound = new InboundServer(defConfig); - const reqInbound = supertest(await serverInbound.setupApi()); + const serverInbound = new InboundServer(defConfig, logger, cache); await serverInbound.start(); + const reqInbound = supertest(serverInbound._server); return { serverOutbound, diff --git a/src/test/unit/config.test.js b/src/test/unit/config.test.js index dd97c4968..ffdb69ae8 100644 --- a/src/test/unit/config.test.js +++ b/src/test/unit/config.test.js @@ -67,7 +67,7 @@ describe('config', () => { fs.writeFileSync(cert, certContent); process.env.IN_SERVER_CERT_PATH = cert; const config = require('../../config'); - const content = config.tls.inbound.creds.cert.toString(); + const content = config.inbound.tls.creds.cert.toString(); expect(content).toBe(certContent); }); @@ -83,7 +83,7 @@ describe('config', () => { certs.forEach((cert, index) => fs.writeFileSync(cert, certContent[index])); process.env.IN_CA_CERT_PATH = certs.join(','); const config = require('../../config'); - const content = config.tls.inbound.creds.ca.map(ca => ca.toString()); + const content = config.inbound.tls.creds.ca.map(ca => ca.toString()); expect(content).toStrictEqual(certContent); }); @@ -93,4 +93,27 @@ describe('config', () => { const proxyConfig = require('./data/testFile'); expect(config.proxyConfig).toEqual(proxyConfig); }); + + it('should transform correctly resources versions to config', () => { + + const resourceVersions = { + resourceOneName: { + acceptVersion: '1', + contentVersion: '1.0', + }, + resourceTwoName: { + acceptVersion: '1', + contentVersion: '1.1', + }, + + }; + const parseResourceVersion = require('../../config').__parseResourceVersion; + expect(parseResourceVersion('resourceOneName=1.0,resourceTwoName=1.1')).toEqual(resourceVersions); + }); + + it('should throw an err if the resource string is not correctly formed', () => { + const parseResourceVersion = require('../../config').__parseResourceVersion; + expect(() => parseResourceVersion('resourceOneName=1.0;resourceTwoName=1.1')).toThrowError(new Error('Resource versions format should be in format: "resouceOneName=1.0,resourceTwoName=1.1"')); + }); + }); diff --git a/src/test/unit/data/defaultConfig.json b/src/test/unit/data/defaultConfig.json index 96c674de3..9c972deac 100644 --- a/src/test/unit/data/defaultConfig.json +++ b/src/test/unit/data/defaultConfig.json @@ -1,35 +1,26 @@ { - "inboundPort": 4000, - "outboundPort": 4001, - "peerEndpoint": "172.17.0.2:3001", - "backendEndpoint": "172.17.0.2:3001", - "alsEndpoint": "127.0.0.1:6500", - "dfspId": "mojaloop-sdk", - "ilpSecret": "mojaloop-sdk", - "checkIlp": false, - "expirySeconds": 60, - "requestProcessingTimeoutSeconds": 30, - "autoAcceptQuotes": true, - "autoAcceptParty": true, - "useQuoteSourceFSPAsTransferPayeeFSP": false, - "tls": { - "test": { + "test": { + "tls": { "mutualTLS": { "enabled": false }, "creds": { "ca": null, "cert": null, "key": null } - }, - "inbound": { + } + }, + "inbound": { + "tls": { "mutualTLS": { "enabled": false }, "creds": { "ca": null, "cert": null, "key": null } - }, - "outbound": { + } + }, + "outbound": { + "tls": { "mutualTLS": { "enabled": false }, "creds": { "ca": null, @@ -38,6 +29,19 @@ } } }, + "peerEndpoint": "172.17.0.2:3001", + "backendEndpoint": "172.17.0.2:3001", + "alsEndpoint": "127.0.0.1:6500", + "dfspId": "mojaloop-sdk", + "ilpSecret": "mojaloop-sdk", + "checkIlp": false, + "expirySeconds": 60, + "requestProcessingTimeoutSeconds": 30, + "autoAcceptQuotes": true, + "autoAcceptParty": true, + "useQuoteSourceFSPAsTransferPayeeFSP": false, + "tls": { + }, "validateInboundJws": false, "validateInboundPutPartiesJws": false, "jwsSign": false, @@ -50,13 +54,14 @@ }, "enableTestFeatures": false, "testingDisableWSO2AuthStart": true, - "testingDisableServerStart": true, "oauthTestServer": { "enabled": false, "listenPort": 6000 }, - "wso2Auth": { - "refreshSeconds": 3600 + "wso2": { + "auth": { + "refreshSeconds": 3600 + } }, "rejectExpiredQuoteResponses": false, "rejectExpiredTransferFulfils": false, diff --git a/src/test/unit/data/putPartiesBody.json b/src/test/unit/data/putPartiesBody.json index e46c03950..817843bd0 100644 --- a/src/test/unit/data/putPartiesBody.json +++ b/src/test/unit/data/putPartiesBody.json @@ -1,5 +1,19 @@ { "party": { + "accounts": { + "account": [ + { + "currency": "USD", + "description": "savings", + "address": "moja.red.8f027046-b82a5456-4fa9-838b-70210fcf8136" + }, + { + "currency": "USD", + "description": "checkings", + "address": "moja.red.8f027046-b8236345a-4fa9-838b-70210fcf8137" + } + ] + }, "partyIdInfo": { "partyIdType": "PERSONAL_ID", "partyIdentifier": "987654321", @@ -17,4 +31,4 @@ "name": "John Doe", "merchantClassificationCode": "1234" } -} +} \ No newline at end of file diff --git a/src/test/unit/inboundApi/data/mockArguments.json b/src/test/unit/inboundApi/data/mockArguments.json index 6a75bdc31..88dcfc348 100644 --- a/src/test/unit/inboundApi/data/mockArguments.json +++ b/src/test/unit/inboundApi/data/mockArguments.json @@ -24,5 +24,94 @@ "name": "Murthy Kakarlamudi", "merchantClassificationCode": "123" } + }, + "bulkQuoteRequest": { + "bulkQuoteId": "fake-bulk-quote-id", + "payer": { + "partyIdInfo": { + "partyIdType": "MSISDN", + "partyIdentifier": "17855501914", + "fspId": "mojaloop-sdk" + }, + "personalInfo": { + "complexName": { + "firstName": "Donald", + "lastName": "Trump" + }, + "dateOfBirth": "2010-10-10" + }, + "name": "Donald Trump", + "merchantClassificationCode": "123" + }, + "individualQuotes": [ + { + "quoteId": "fake-bulk-quote-id", + "transactionId": "fake-transaction-id", + "payee": { + "partyIdInfo": { + "partyIdType": "MSISDN", + "partyIdentifier": "17855508275", + "fspId": "mojaloop-sdk" + }, + "personalInfo": { + "complexName": { + "firstName": "Justin", + "lastName": "Trudeau" + }, + "dateOfBirth": "2010-10-10" + }, + "name": "Justin Trudeau", + "merchantClassificationCode": "123" + }, + "amountType": "SEND", + "amount": { + "currency": "XOF", + "amount": 10 + }, + "transactionType": { + "scenario": "TRANSFER", + "initiator": "PAYER", + "initiatorType": "CONSUMER" + } + } + ], + "expiration": "2019-06-04T04:02:10.378Z" + }, + "bulkQuotePutRequest": { + "expiration": "2019-06-04T04:02:10.378Z", + "individualQuoteResults": [ + { + "quoteId": "fake-bulk-transfer-id", + "ilpPacket": "WLctttbu2HvTsa1XWvUoGRcQozHsqeu9Ahl2JW9Bsu8", + "condition": "fake-condition" + } + ] + }, + "bulkTransferRequest": { + "bulkTransferId": "fake-bulk-transfer-id", + "bulkQuoteId": "fake-bulk-quote-id", + "payerFsp": "fake-payer-fsp", + "payeeFsp": "fake-payee-fsp", + "expiration": "2019-06-04T04:02:10.378Z", + "individualTransfers": [ + { + "transferId": "fake-bulk-transfer-id", + "transferAmount": { + "currency": "XOF", + "amount": 10 + }, + "ilpPacket": "AYIBgQAAAAAAAASwNGxldmVsb25lLmRmc3AxLm1lci45T2RTOF81MDdqUUZERmZlakgyOVc4bXFmNEpLMHlGTFGCAUBQU0svMS4wCk5vbmNlOiB1SXlweUYzY3pYSXBFdzVVc05TYWh3CkVuY3J5cHRpb246IG5vbmUKUGF5bWVudC1JZDogMTMyMzZhM2ItOGZhOC00MTYzLTg0NDctNGMzZWQzZGE5OGE3CgpDb250ZW50LUxlbmd0aDogMTM1CkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbgpTZW5kZXItSWRlbnRpZmllcjogOTI4MDYzOTEKCiJ7XCJmZWVcIjowLFwidHJhbnNmZXJDb2RlXCI6XCJpbnZvaWNlXCIsXCJkZWJpdE5hbWVcIjpcImFsaWNlIGNvb3BlclwiLFwiY3JlZGl0TmFtZVwiOlwibWVyIGNoYW50XCIsXCJkZWJpdElkZW50aWZpZXJcIjpcIjkyODA2MzkxXCJ9IgA", + "condition": "f5sqb7tBTWPd5Y8BDFdMm9BJR_MNI4isf8p8n4D5pHA" + } + ] + }, + "bulkTransferPutRequest": { + "completedTimestamp": "2019-06-04T04:02:10.378Z", + "individualTransferResults": [ + { + "transferId": "fake-bulk-transfer-id", + "fulfilment": "WLctttbu2HvTsa1XWvUoGRcQozHsqeu9Ahl2JW9Bsu8" + } + ] } -} +} \ No newline at end of file diff --git a/src/test/unit/inboundApi/handlers.test.js b/src/test/unit/inboundApi/handlers.test.js index 6ab9ebae3..8c1611586 100644 --- a/src/test/unit/inboundApi/handlers.test.js +++ b/src/test/unit/inboundApi/handlers.test.js @@ -6,6 +6,8 @@ * * * ORIGINAL AUTHOR: * * Vassilis Barzokas - vassilis.barzokas@modusbox.com * + * Paweł Marzec - pawel.marzec@modusbox.com * + * Sridhar Voruganti - sridhar.voruganti@modusbox.com * **************************************************************************/ 'use strict'; @@ -16,28 +18,30 @@ const handlers = require('../../../InboundServer/handlers'); const Model = require('@internal/model').InboundTransfersModel; const mockArguments = require('./data/mockArguments'); const mockTransactionRequestData = require('./data/mockTransactionRequest'); -const { Logger, Transports } = require('@internal/log'); - -let logTransports; +const mockAuthorizationArguments = require('../lib/model/data/mockAuthorizationArguments.json'); +// TODO: decide between logger implementations +const mockLogger = require('../mockLogger'); +const { Logger } = require('@mojaloop/sdk-standard-components'); +const AuthorizationsModel = require('@internal/model').OutboundAuthorizationsModel; +const ThirdpartyTrxnModelIn = require('@internal/model').InboundThirdpartyTransactionModel; +const ThirdpartyTrxnModelOut = require('@internal/model').OutboundThirdpartyTransactionModel; +const PartiesModel = require('@internal/model').PartiesModel; describe('Inbound API handlers:', () => { let mockArgs; let mockTransactionRequest; - - beforeAll(async () => { - logTransports = await Promise.all([Transports.consoleDir()]); - }); + let mockAuthReqArgs; beforeEach(() => { - mockArgs = JSON.parse(JSON.stringify(mockArguments)); - mockTransactionRequest = JSON.parse(JSON.stringify(mockTransactionRequestData)); - + mockArgs = deepClone(mockArguments); + mockTransactionRequest = deepClone(mockTransactionRequestData); + mockAuthReqArgs = deepClone(mockAuthorizationArguments); }); describe('POST /quotes', () => { - + let mockContext; - + beforeEach(() => { mockContext = { request: { @@ -49,10 +53,10 @@ describe('Inbound API handlers:', () => { response: {}, state: { conf: {}, - logger: new Logger({ context: { app: 'inbound-handlers-unit-test' }, space: 4, transports: logTransports }) + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) } }; - + }); test('calls `model.quoteRequest` with the expected arguments.', async () => { @@ -61,19 +65,372 @@ describe('Inbound API handlers:', () => { await expect(handlers['/quotes'].post(mockContext)).resolves.toBe(undefined); expect(quoteRequestSpy).toHaveBeenCalledTimes(1); - expect(quoteRequestSpy.mock.calls[0][0]).toBe(mockContext.request.body); - expect(quoteRequestSpy.mock.calls[0][1]).toBe(mockContext.request.headers['fspiop-source']); + expect(quoteRequestSpy).toHaveBeenCalledWith( + mockContext.request.body, + mockContext.request.headers['fspiop-source']); + }); + + + }); + + describe('GET /quotes', () => { + + let mockContext; + + beforeEach(() => { + mockContext = { + request: { + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: new Logger.Logger({ context: { app: 'inbound-handlers-unit-test' }, stringify: () => '' }), + } + }; + + }); + + test('calls `model.getQuoteRequest` with the expected arguments.', async () => { + const getQuoteRequestSpy = jest.spyOn(Model.prototype, 'getQuoteRequest'); + + await expect(handlers['/quotes/{ID}'].get(mockContext)).resolves.toBe(undefined); + + expect(getQuoteRequestSpy).toHaveBeenCalledTimes(1); + expect(getQuoteRequestSpy.mock.calls[0][1]).toBe(mockContext.request.headers['fspiop-source']); + + }); + + + }); + + describe('POST /bulkQuotes', () => { + + let mockContext; + + beforeEach(() => { + mockContext = { + request: { + body: mockArgs.bulkQuoteRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) + } + }; + + }); + + test('calls `model.bulkQuoteRequest` with the expected arguments.', async () => { + const bulkQuoteRequestSpy = jest.spyOn(Model.prototype, 'bulkQuoteRequest'); + + await expect(handlers['/bulkQuotes'].post(mockContext)).resolves.toBe(undefined); + + expect(bulkQuoteRequestSpy).toHaveBeenCalledTimes(1); + expect(bulkQuoteRequestSpy.mock.calls[0][0]).toBe(mockContext.request.body); + expect(bulkQuoteRequestSpy.mock.calls[0][1]).toBe(mockContext.request.headers['fspiop-source']); + }); + }); + + describe('PUT /bulkQuotes/{ID}', () => { + + let mockContext; + + beforeEach(() => { + + mockContext = { + request: { + body: mockArgs.bulkQuotePutRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: async () => Promise.resolve(true) + } + } + }; + }); + + test('calls `ctx.state.cache.publish` with the expected arguments.', async () => { + const bulkQuotesSpy = jest.spyOn(mockContext.state.cache, 'publish'); + + await expect(handlers['/bulkQuotes/{ID}'].put(mockContext)).resolves.toBe(undefined); + expect(mockContext.response.status).toBe(200); + expect(bulkQuotesSpy).toHaveBeenCalledTimes(1); + expect(bulkQuotesSpy.mock.calls[0][1]).toMatchObject({ + type: 'bulkQuoteResponse', + data: mockContext.request.body, + headers: mockContext.request.headers + }); + }); + }); + + describe('PUT /bulkQuotes/{ID}/error', () => { + + let mockContext; + + beforeEach(() => { + + mockContext = { + request: { + body: { + errorInformation: { + errorCode: '5100', + errorDescription: 'Fake error' + } + }, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: async () => Promise.resolve(true) + } + } + }; + }); + + test('calls `ctx.state.cache.publish` with the expected arguments.', async () => { + const bulkQuotesSpy = jest.spyOn(mockContext.state.cache, 'publish'); + + await expect(handlers['/bulkQuotes/{ID}/error'].put(mockContext)).resolves.toBe(undefined); + expect(mockContext.response.status).toBe(200); + expect(bulkQuotesSpy).toHaveBeenCalledTimes(1); + expect(bulkQuotesSpy.mock.calls[0][1]).toMatchObject({ + type: 'bulkQuoteResponseError', + data: mockContext.request.body + }); + }); + }); + + describe('GET /bulkQuotes/{ID}', () => { + + let mockContext; + + beforeEach(() => { + + mockContext = { + request: { + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) + } + }; + }); + + test('calls `model.getBulkQuote` with the expected arguments.', async () => { + const bulkQuotesSpy = jest.spyOn(Model.prototype, 'getBulkQuote'); + + await expect(handlers['/bulkQuotes/{ID}'].get(mockContext)).resolves.toBe(undefined); + + expect(bulkQuotesSpy).toHaveBeenCalledTimes(1); + expect(bulkQuotesSpy.mock.calls[0][1]).toBe(mockContext.request.headers['fspiop-source']); + }); + }); + + describe('POST /bulkTransfers', () => { + + let mockContext; + + beforeEach(() => { + mockContext = { + request: { + body: mockArgs.bulkTransferRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) + } + }; + + }); + + test('calls `model.prepareBulkTransfer` with the expected arguments.', async () => { + const bulkTransferRequestSpy = jest.spyOn(Model.prototype, 'prepareBulkTransfer'); + + await expect(handlers['/bulkTransfers'].post(mockContext)).resolves.toBe(undefined); + + expect(bulkTransferRequestSpy).toHaveBeenCalledTimes(1); + expect(bulkTransferRequestSpy.mock.calls[0][0]).toBe(mockContext.request.body); + expect(bulkTransferRequestSpy.mock.calls[0][1]).toBe(mockContext.request.headers['fspiop-source']); + }); + }); + + describe('PUT /bulkTransfers/{ID}', () => { + + let mockContext; + + beforeEach(() => { + + mockContext = { + request: { + body: mockArgs.bulkTransferPutRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: async () => Promise.resolve(true) + } + } + }; + }); + + test('calls `ctx.state.cache.publish` with the expected arguments.', async () => { + const bulkTransfersSpy = jest.spyOn(mockContext.state.cache, 'publish'); + + await expect(handlers['/bulkTransfers/{ID}'].put(mockContext)).resolves.toBe(undefined); + expect(mockContext.response.status).toBe(200); + expect(bulkTransfersSpy).toHaveBeenCalledTimes(1); + expect(bulkTransfersSpy.mock.calls[0][1]).toMatchObject({ + type: 'bulkTransferResponse', + data: mockContext.request.body, + headers: mockContext.request.headers + }); + }); + }); + + describe('PUT /bulkTransfers/{ID}/error', () => { + + let mockContext; + + beforeEach(() => { + + mockContext = { + request: { + body: { + errorInformation: { + errorCode: '5100', + errorDescription: 'Fake error' + } + }, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: async () => Promise.resolve(true) + } + } + }; + }); + + test('calls `ctx.state.cache.publish` with the expected arguments.', async () => { + const bulkTransfersSpy = jest.spyOn(mockContext.state.cache, 'publish'); + + await expect(handlers['/bulkTransfers/{ID}/error'].put(mockContext)).resolves.toBe(undefined); + expect(mockContext.response.status).toBe(200); + expect(bulkTransfersSpy).toHaveBeenCalledTimes(1); + expect(bulkTransfersSpy.mock.calls[0][1]).toMatchObject({ + type: 'bulkTransferResponseError', + data: mockContext.request.body + }); + }); + }); + + describe('GET /bulkTransfers/{ID}', () => { + + let mockContext; + + beforeEach(() => { + + mockContext = { + request: { + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234567890' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) + } + }; }); - + test('calls `model.getBulkTransfer` with the expected arguments.', async () => { + const bulkTransfersSpy = jest.spyOn(Model.prototype, 'getBulkTransfer'); + + await expect(handlers['/bulkTransfers/{ID}'].get(mockContext)).resolves.toBe(undefined); + + expect(bulkTransfersSpy).toHaveBeenCalledTimes(1); + expect(bulkTransfersSpy.mock.calls[0][1]).toBe(mockContext.request.headers['fspiop-source']); + }); }); describe('POST /transactionRequests', () => { - + let mockTransactionReqContext; beforeEach(() => { - + mockTransactionReqContext = { request: { body: mockTransactionRequest.transactionRequest, @@ -84,7 +441,7 @@ describe('Inbound API handlers:', () => { response: {}, state: { conf: {}, - logger: new Logger({ context: { app: 'inbound-handlers-unit-test' }, space: 4, transports: logTransports }) + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) } }; }); @@ -95,17 +452,50 @@ describe('Inbound API handlers:', () => { await expect(handlers['/transactionRequests'].post(mockTransactionReqContext)).resolves.toBe(undefined); expect(transactionRequestSpy).toHaveBeenCalledTimes(1); - expect(transactionRequestSpy.mock.calls[0][0]).toBe(mockTransactionReqContext.request.body); - expect(transactionRequestSpy.mock.calls[0][1]).toBe(mockTransactionReqContext.request.headers['fspiop-source']); + expect(transactionRequestSpy).toHaveBeenCalledWith( + mockTransactionReqContext.request.body, + mockTransactionReqContext.request.headers['fspiop-source']); + }); + }); + + describe('POST /authorizations', () => { + + let mockAuthorizationContext; + + beforeEach(() => { + + mockAuthorizationContext = { + request: { + body: mockAuthReqArgs.authorizationRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) + } + }; + }); + + test('calls `ThirdpartyTrxnModelIn.postAuthorizations` with the expected arguments.', async () => { + const authorizationRequestSpy = jest.spyOn(ThirdpartyTrxnModelIn.prototype, 'postAuthorizations'); + + await expect(handlers['/authorizations'].post(mockAuthorizationContext)).resolves.toBe(undefined); + expect(authorizationRequestSpy).toHaveBeenCalledTimes(1); + expect(authorizationRequestSpy).toHaveBeenCalledWith( + mockAuthorizationContext.request.body, + mockAuthorizationContext.request.headers['fspiop-source']); }); }); describe('GET /authorizations', () => { - + let mockAuthorizationContext; beforeEach(() => { - + mockAuthorizationContext = { request: { headers: { @@ -115,12 +505,12 @@ describe('Inbound API handlers:', () => { response: {}, state: { conf: {}, - path : { - params : { + path: { + params: { 'ID': '1234' } }, - logger: new Logger({ context: { app: 'inbound-handlers-unit-test' }, space: 4, transports: logTransports }) + logger: mockLogger({ app: 'inbound-handlers-unit-test' }) } }; }); @@ -131,7 +521,232 @@ describe('Inbound API handlers:', () => { await expect(handlers['/authorizations/{ID}'].get(mockAuthorizationContext)).resolves.toBe(undefined); expect(authorizationsSpy).toHaveBeenCalledTimes(1); - expect(authorizationsSpy.mock.calls[0][1]).toBe(mockAuthorizationContext.request.headers['fspiop-source']); + expect(authorizationsSpy).toHaveBeenCalledWith( + mockAuthorizationContext.state.path.params.ID, + mockAuthorizationContext.request.headers['fspiop-source']); + }); + }); + + describe('PISP PUT /authorizations', () => { + + let mockAuthorizationContext; + beforeEach(() => { + + mockAuthorizationContext = { + request: { + headers: { + 'fspiop-source': 'foo' + }, + body: { + the: 'body' + } + }, + response: {}, + state: { + conf: { + enablePISPMode: true + }, + path: { + params: { + 'ID': '1234' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + // there is no need to mock redis but only Cache + cache: { + publish: jest.fn(() => Promise.resolve()) + }, + } + }; + }); + + test('calls `model.authorizations` with the expected arguments.', async () => { + const notificationSpy = jest.spyOn(AuthorizationsModel, 'notificationChannel').mockImplementationOnce(() => 'notification-channel'); + + await expect(handlers['/authorizations/{ID}'].put(mockAuthorizationContext)).resolves.toBe(undefined); + + expect(notificationSpy).toHaveBeenCalledTimes(1); + expect(notificationSpy).toHaveBeenCalledWith('1234'); + + const cache = mockAuthorizationContext.state.cache; + expect(cache.publish).toBeCalledTimes(1); + expect(cache.publish).toBeCalledWith('notification-channel', { + type: 'authorizationsResponse', + data: mockAuthorizationContext.request.body, + headers: mockAuthorizationContext.request.headers + }); + }); + }); + + describe('DFSP PUT /authorizations', () => { + + let mockAuthorizationContext; + beforeEach(() => { + + mockAuthorizationContext = { + request: { + headers: { + 'fspiop-source': 'foo' + }, + body: { + the: 'body' + } + }, + response: {}, + state: { + conf: { + enablePISPMode: false + }, + path: { + params: { + 'ID': '1234' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + // there is no need to mock redis but only Cache + cache: { + publish: jest.fn(() => Promise.resolve()) + }, + } + }; + }); + + test('calls `model.authorizations` with the expected arguments.', async () => { + + await expect(handlers['/authorizations/{ID}'].put(mockAuthorizationContext)).resolves.toBe(undefined); + + const cache = mockAuthorizationContext.state.cache; + expect(cache.publish).toBeCalledTimes(1); + expect(cache.publish).toBeCalledWith('otp_1234', { + type: 'authorizationsResponse', + data: mockAuthorizationContext.request.body, + headers: mockAuthorizationContext.request.headers + }); + }); + }); + + describe('PUT /thirdpartyRequests/transactions', () => { + let mockThirdPartyReqContext; + beforeEach(() => { + mockThirdPartyReqContext = { + request: { + headers: { + 'fspiop-source': 'foo' + }, + body: { + the: 'body' + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'ID': '1234' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: jest.fn(() => Promise.resolve()) + }, + } + }; + }); + + test('calls `model.thirdpartyRequests.transactions` with the expected arguments.', async () => { + const pubNotificatiosnSpy = jest.spyOn(ThirdpartyTrxnModelOut, 'publishNotifications'); + + await expect(handlers['/thirdpartyRequests/transactions/{ID}'].put(mockThirdPartyReqContext)).resolves.toBe(undefined); + + expect(pubNotificatiosnSpy).toHaveBeenCalledTimes(1); + expect(pubNotificatiosnSpy).toHaveBeenCalledWith(mockThirdPartyReqContext.state.cache, + mockThirdPartyReqContext.state.path.params.ID, { + type: 'thirdpartyTransactionsReqResponse', + data: mockThirdPartyReqContext.request.body, + headers: mockThirdPartyReqContext.request.headers + }); + }); + }); + + describe('PUT /parties/{Type}/{ID}', () => { + let mockPutPartiesCtx; + beforeEach(() => { + mockPutPartiesCtx = { + request: { + headers: { + 'fspiop-source': 'foo' + }, + body: { + party: { Iam: 'mocked-party' } + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'Type': 'MSISDN', + 'ID': '123456789' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: jest.fn(() => Promise.resolve()) + }, + } + }; + }); + + test('calls cache.publish with the expected arguments.', async () => { + PartiesModel.channelName = jest.fn(() => 'mocked-parties-channel'); + await expect(handlers['/parties/{Type}/{ID}'].put(mockPutPartiesCtx)).resolves.toBe(undefined); + expect(mockPutPartiesCtx.state.cache.publish).toHaveBeenCalledWith('MSISDN_123456789', {party: { Iam: 'mocked-party' }}); + expect(mockPutPartiesCtx.state.cache.publish).toHaveBeenCalledWith('mocked-parties-channel', {party: { Iam: 'mocked-party' }}); + expect(mockPutPartiesCtx.response.status).toBe(200); + }); + }); + + describe('PUT /parties/{Type}/{ID}/{SubId}', () => { + let mockPutPartiesCtx; + beforeEach(() => { + mockPutPartiesCtx = { + request: { + headers: { + 'fspiop-source': 'foo' + }, + body: { + party: { Iam: 'mocked-party' } + } + }, + response: {}, + state: { + conf: {}, + path: { + params: { + 'Type': 'MSISDN', + 'ID': '123456789', + 'SubId': 'abcdefg' + } + }, + logger: mockLogger({ app: 'inbound-handlers-unit-test' }), + cache: { + publish: jest.fn(() => Promise.resolve()) + }, + } + }; + }); + + test('calls cache.publish with the expected arguments.', async () => { + PartiesModel.channelName = jest.fn(() => 'mocked-parties-channel'); + await expect(handlers['/parties/{Type}/{ID}'].put(mockPutPartiesCtx)).resolves.toBe(undefined); + expect(mockPutPartiesCtx.state.cache.publish).toHaveBeenCalledWith('MSISDN_123456789_abcdefg', {party: { Iam: 'mocked-party' }}); + expect(mockPutPartiesCtx.state.cache.publish).toHaveBeenCalledWith('mocked-parties-channel', {party: { Iam: 'mocked-party' }}); + expect(mockPutPartiesCtx.response.status).toBe(200); }); }); + }); + +function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/src/test/unit/index.test.js b/src/test/unit/index.test.js index 1cbbf735c..bcc435094 100644 --- a/src/test/unit/index.test.js +++ b/src/test/unit/index.test.js @@ -10,6 +10,9 @@ 'use strict'; +const { Logger } = require('@mojaloop/sdk-standard-components'); +const defaultConfig = require('./data/defaultConfig'); + jest.mock('dotenv', () => ({ config: jest.fn() })); @@ -21,8 +24,25 @@ process.env.CACHE_PORT = '6379'; const index = require('../../index.js'); - describe('index.js', () => { + test('WSO2 error events in OutboundServer propagate to top-level server', () => { + const logger = new Logger.Logger({ stringify: () => '' }); + const svr = new index.Server(defaultConfig, logger); + const cb = jest.fn(); + svr.on('error', cb); + svr.outboundServer._api._wso2.auth.emit('error', 'msg'); + expect(cb).toHaveBeenCalledTimes(1); + }); + + test('WSO2 error events in InboundServer propagate to top-level server', () => { + const logger = new Logger.Logger({ stringify: () => '' }); + const svr = new index.Server(defaultConfig, logger); + const cb = jest.fn(); + svr.on('error', cb); + svr.inboundServer._api._wso2.auth.emit('error', 'msg'); + expect(cb).toHaveBeenCalledTimes(1); + }); + test('Exports expected modules', () => { expect(typeof(index.Server)).toBe('function'); expect(typeof(index.InboundServerMiddleware)).toBe('object'); @@ -30,7 +50,6 @@ describe('index.js', () => { expect(typeof(index.Router)).toBe('function'); expect(typeof(index.Validate)).toBe('function'); expect(typeof(index.RandomPhrase)).toBe('function'); - expect(typeof(index.Log)).toBe('object'); expect(typeof(index.Cache)).toBe('function'); }); }); diff --git a/src/test/unit/lib/cache/cache.test.js b/src/test/unit/lib/cache/cache.test.js index eb657a9ff..8c7bb5921 100644 --- a/src/test/unit/lib/cache/cache.test.js +++ b/src/test/unit/lib/cache/cache.test.js @@ -13,11 +13,10 @@ jest.mock('redis'); const Cache = require('@internal/cache'); -const { Logger } = require('@internal/log'); +const { Logger } = require('@mojaloop/sdk-standard-components'); const createCache = async() => { - const logTransports = [() => {}]; - const logger = new Logger({ context: { app: 'model-unit-tests-cache' }, space: 4, transports: logTransports }); + const logger = new Logger.Logger({ context: { app: 'model-unit-tests-cache' }, stringify: () => '' }); const cache = new Cache({ host: 'dummyhost', port: 1234, @@ -59,9 +58,8 @@ describe('Cache', () => { // create a promise that only gets resoled if the subscription gets the // correct message const cb1Promise = new Promise((resolve) => { - const mockCb1 = jest.fn((cn, msg, subId) => { + const mockCb1 = jest.fn((cn, msg) => { expect(cn).toBe(chan1); - console.log(`callback on subId: ${subId}`); const value = JSON.parse(msg); // check we got the expected message @@ -84,9 +82,8 @@ describe('Cache', () => { // create a second promise that only gets resoled if the second subscription gets the // correct message const cb2Promise = new Promise((resolve) => { - const mockCb2 = jest.fn((cn, msg, subId) => { + const mockCb2 = jest.fn((cn, msg) => { expect(cn).toBe(chan2); - console.log(`callback on subId: ${subId}`); // check we got the expected message const value = JSON.parse(msg); diff --git a/src/test/unit/lib/log/log.test.js b/src/test/unit/lib/log/log.test.js deleted file mode 100644 index ece7a6222..000000000 --- a/src/test/unit/lib/log/log.test.js +++ /dev/null @@ -1,76 +0,0 @@ -/************************************************************************** - * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * - * * - * This file is made available under the terms of the license agreement * - * specified in the corresponding source code repository. * - * * - * ORIGINAL AUTHOR: * - * James Bush - james.bush@modusbox.com * - **************************************************************************/ - -'use strict'; - - -const { Logger, Transports } = require('@internal/log'); - - - -describe('Logger', () => { - - test('logs non-circular without throwing', async () => { - const transports = await Promise.all([Transports.consoleDir()]); - - let logger = new Logger({ - context: { - app: 'test' - }, - space: 4, - transports, - }); - - const testOb = { - a: 'test', - b: 123, - c: [1, 2, 3] - }; - - try { - logger = logger.push(testOb); - await logger.log('This is a test'); - } - catch(e) { - expect(e).toBe(undefined); - } - }); - - - test('logs circular without throwing', async () => { - const transports = await Promise.all([Transports.consoleDir()]); - - let logger = new Logger({ - context: { - app: 'test' - }, - space: 4, - transports, - }); - - const testOb = { - a: 'test', - b: 123, - c: [1, 2, 3] - }; - - // create a circular reference in testOb - testOb.d = testOb; - - try { - logger = logger.push(testOb); - await logger.log('This is a test'); - } - catch(e) { - expect(e).toBe(undefined); - } - }); - -}); diff --git a/src/test/unit/lib/model/AccountsModel.test.js b/src/test/unit/lib/model/AccountsModel.test.js index e6ebb44b8..66b0c70ad 100644 --- a/src/test/unit/lib/model/AccountsModel.test.js +++ b/src/test/unit/lib/model/AccountsModel.test.js @@ -15,11 +15,10 @@ jest.mock('@mojaloop/sdk-standard-components'); jest.mock('redis'); const Cache = require('@internal/cache'); -const { Logger } = require('@internal/log'); const { AccountsModel } = require('@internal/model'); const StateMachine = require('javascript-state-machine'); -const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components'); const defaultConfig = require('./data/defaultConfig'); const transferRequest = require('./data/transferRequest'); @@ -65,6 +64,7 @@ describe('AccountsModel', () => { const model = new AccountsModel({ ...defaultConfig, + tls: defaultConfig.outbound.tls, cache, logger, }); @@ -77,8 +77,6 @@ describe('AccountsModel', () => { // wait for the model to reach a terminal state const result = await model.run(); - // console.log(`Accounts creation result: ${util.inspect(result)}`); - const expectedRequestsCount = currencies.length * (Math.floor(count / MAX_ITEMS_PER_REQUEST) + ((count % MAX_ITEMS_PER_REQUEST) ? 1 : 0)); expect(MojaloopRequests.__postParticipants).toHaveBeenCalledTimes(expectedRequestsCount); @@ -88,8 +86,7 @@ describe('AccountsModel', () => { } beforeAll(() => { - const logTransports = [() => {}]; - logger = new Logger({ context: { app: 'outbound-model-unit-tests-cache' }, space: 4, transports: logTransports }); + logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' }); }); beforeEach(async () => { @@ -108,6 +105,7 @@ describe('AccountsModel', () => { test('initializes to starting state', async () => { const model = new AccountsModel({ ...defaultConfig, + tls: defaultConfig.outbound.tls, cache, logger, }); diff --git a/src/test/unit/lib/model/InboundThirdpartyTransactionModel.test.js b/src/test/unit/lib/model/InboundThirdpartyTransactionModel.test.js new file mode 100644 index 000000000..f65b92ada --- /dev/null +++ b/src/test/unit/lib/model/InboundThirdpartyTransactionModel.test.js @@ -0,0 +1,63 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Sridhar Voruganti - sridhar.voruganti@modusbox.com * + **************************************************************************/ +'use strict'; + +// we use a mock standard components lib to intercept and mock certain funcs +jest.mock('@mojaloop/sdk-standard-components'); + +const defaultConfig = require('./data/defaultConfig'); +const ThirdpartyTrxnModelIn = require('@internal/model').InboundThirdpartyTransactionModel; +const mockAuthorizationArguments = require('./data/mockAuthorizationArguments'); +const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const { BackendRequests } = require('@internal/requests'); +const mockLogger = require('../../mockLogger'); + +describe('inboundThirdpartyTransactionModel', () => { + let config; + let mockAuthReqArgs; + let logger; + + beforeEach(async () => { + config = deepClone(defaultConfig); + mockAuthReqArgs = deepClone(mockAuthorizationArguments); + logger = mockLogger({ app: 'InboundThirdpartyTransactionModel-test' }); + }); + + describe('authorizations', () => { + let model; + + beforeEach(async () => { + BackendRequests.__getSignedChallenge = jest.fn().mockReturnValue( + Promise.resolve(mockAuthReqArgs.internalSignedChallengeResponse)); + + model = new ThirdpartyTrxnModelIn({ + ...config, + logger + }); + }); + + afterEach(async () => { + MojaloopRequests.__putAuthorizations.mockClear(); + }); + + test('calls `mojaloopRequests.putAuthorizations` with the expected arguments.', async () => { + await model.postAuthorizations(mockAuthReqArgs.authorizationRequest, mockAuthReqArgs.fspId); + expect(MojaloopRequests.__putAuthorizations).toHaveBeenCalledTimes(1); + expect(MojaloopRequests.__putAuthorizations).toHaveBeenCalledWith( + mockAuthReqArgs.authorizationRequest.transactionRequestId, + mockAuthReqArgs.authorizationsResponse, mockAuthReqArgs.fspId); + }); + }); + +}); + +function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/src/test/unit/lib/model/InboundTransfersModel.test.js b/src/test/unit/lib/model/InboundTransfersModel.test.js index 5ab1719b1..8102a1abd 100644 --- a/src/test/unit/lib/model/InboundTransfersModel.test.js +++ b/src/test/unit/lib/model/InboundTransfersModel.test.js @@ -14,16 +14,18 @@ jest.mock('@mojaloop/sdk-standard-components'); jest.mock('redis'); const defaultConfig = require('./data/defaultConfig'); -const { Logger, Transports } = require('@internal/log'); const Model = require('@internal/model').InboundTransfersModel; const mockArguments = require('./data/mockArguments'); const mockTxnReqquestsArguments = require('./data/mockTxnRequestsArguments'); -const { MojaloopRequests, Ilp } = require('@mojaloop/sdk-standard-components'); +const { MojaloopRequests, Ilp, Logger } = require('@mojaloop/sdk-standard-components'); const { BackendRequests, HTTPResponseError } = require('@internal/requests'); const Cache = require('@internal/cache'); const getTransfersBackendResponse = require('./data/getTransfersBackendResponse'); const getTransfersMojaloopResponse = require('./data/getTransfersMojaloopResponse'); +const getBulkTransfersBackendResponse = require('./data/getBulkTransfersBackendResponse'); +const getBulkTransfersMojaloopResponse = require('./data/getBulkTransfersMojaloopResponse'); +const notificationToPayee = require('./data/notificationToPayee'); describe('inboundModel', () => { let config; @@ -32,12 +34,7 @@ describe('inboundModel', () => { let logger; beforeAll(async () => { - const logTransports = await Promise.all([Transports.consoleDir()]); - logger = new Logger({ - context: { app: 'inbound-model-unit-tests' }, - space: 4, - transports: logTransports, - }); + logger = new Logger.Logger({ context: { app: 'inbound-model-unit-tests' }, stringify: () => '' }); }); beforeEach(async () => { @@ -109,6 +106,66 @@ describe('inboundModel', () => { }); + describe('bulkQuoteRequest', () => { + let expectedQuoteResponseILP; + let model; + let cache; + + beforeEach(async () => { + // eslint-disable-next-line no-unused-vars + expectedQuoteResponseILP = Ilp.__response; + BackendRequests.__postBulkQuotes = jest.fn().mockReturnValue(Promise.resolve(mockArgs.internalBulkQuoteResponse)); + + cache = new Cache({ + host: 'dummycachehost', + port: 1234, + logger, + }); + await cache.connect(); + // eslint-disable-next-line no-unused-vars + model = new Model({ + ...config, + cache, + logger, + }); + }); + + afterEach(async () => { + MojaloopRequests.__putBulkQuotes.mockClear(); + await cache.disconnect(); + }); + + test('calls mojaloopRequests.putBulkQuotes with the expected arguments.', async () => { + await model.bulkQuoteRequest(mockArgs.bulkQuoteRequest, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkQuotes).toHaveBeenCalledTimes(1); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][1].expiration).toBe(mockArgs.internalBulkQuoteResponse.expiration); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][1].individualQuoteResults[0].ilpPacket).toBe(expectedQuoteResponseILP.ilpPacket); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][1].individualQuoteResults[0].condition).toBe(expectedQuoteResponseILP.condition); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][2]).toBe(mockArgs.fspId); + }); + test('adds a custom expiration property in case it is not defined.', async() => { + // set a custom mock time in the global Date object in order to avoid race conditions. + // Make sure to clear it at the end of the test case. + const currentTime = new Date().getTime(); + const dateSpy = jest.spyOn(Date.prototype, 'getTime').mockImplementation(() => currentTime); + const expectedExpirationDate = new Date(currentTime + (config.expirySeconds * 1000)).toISOString(); + + delete mockArgs.internalBulkQuoteResponse.expiration; + + await model.bulkQuoteRequest(mockArgs.bulkQuoteRequest, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkQuotes).toHaveBeenCalledTimes(1); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][1].expiration).toBe(expectedExpirationDate); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][1].individualQuoteResults[0].ilpPacket).toBe(expectedQuoteResponseILP.ilpPacket); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][1].individualQuoteResults[0].condition).toBe(expectedQuoteResponseILP.condition); + expect(MojaloopRequests.__putBulkQuotes.mock.calls[0][2]).toBe(mockArgs.fspId); + + dateSpy.mockClear(); + }); + + }); + describe('transactionRequest', () => { let model; let cache; @@ -276,9 +333,9 @@ describe('inboundModel', () => { BackendRequests.__getTransfers = jest.fn().mockReturnValue( Promise.reject(new HTTPResponseError({ res: { - body: JSON.stringify({ + data: { statusCode: '3208' - }), + }, } }))); @@ -349,4 +406,222 @@ describe('inboundModel', () => { expect(MojaloopRequests.__putTransfers).toHaveBeenCalledTimes(1); }); }); + + describe('prepareBulkTransfer:', () => { + let cache; + + beforeEach(async () => { + MojaloopRequests.__putBulkTransfersError.mockClear(); + MojaloopRequests.__putBulkTransfers = jest.fn().mockReturnValue(Promise.resolve({})); + BackendRequests.__postBulkTransfers = jest.fn().mockReturnValue(Promise.resolve({})); + + cache = new Cache({ + host: 'dummycachehost', + port: 1234, + logger, + }); + await cache.connect(); + }); + + afterEach(async () => { + await cache.disconnect(); + }); + + test('fail on bulk quote `expiration` deadline.', async () => { + const BULK_TRANSFER_ID = 'fake-bulk-transfer-id'; + const BULK_QUOTE_ID = 'fake-bulk-quote-id'; + const model = new Model({ + ...config, + cache, + logger, + rejectTransfersOnExpiredQuotes: true, + }); + cache.set(`bulkQuotes_${BULK_QUOTE_ID}`, { + mojaloopResponse: { + expiration: new Date(new Date().getTime() - 1000).toISOString(), + individualQuoteResults: [], + } + }); + const args = { + bulkTransferId: BULK_TRANSFER_ID, + bulkQuoteId: BULK_QUOTE_ID, + individualTransfers: [], + }; + + await model.prepareBulkTransfer(args, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkTransfersError).toHaveBeenCalledTimes(1); + const call = MojaloopRequests.__putBulkTransfersError.mock.calls[0]; + expect(call[0]).toEqual(BULK_TRANSFER_ID); + expect(call[1].errorInformation.errorCode).toEqual('3302'); + }); + + test('getBulkTransfer should return COMMITTED bulk transfer', async () => { + const BULK_TRANSFER_ID = 'fake-bulk-transfer-id'; + + const backendResponse = JSON.parse(JSON.stringify(getBulkTransfersBackendResponse)); + BackendRequests.__getBulkTransfers = jest.fn().mockReturnValue(Promise.resolve(backendResponse)); + + const model = new Model({ + ...config, + cache, + logger, + }); + + await model.getBulkTransfer(BULK_TRANSFER_ID, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkTransfers).toHaveBeenCalledTimes(1); + const call = MojaloopRequests.__putBulkTransfers.mock.calls[0]; + expect(call[0]).toEqual(BULK_TRANSFER_ID); + expect(call[1]).toEqual(getBulkTransfersMojaloopResponse); + expect(call[1].bulkTransferState).toEqual('COMMITTED'); + }); + + test('getBulkTransfer should not return fulfillment from payer', async () => { + const BULK_TRANSFER_ID = 'fake-bulk-transfer-id'; + + const backendResponse = JSON.parse(JSON.stringify(getBulkTransfersBackendResponse)); + backendResponse.internalRequest.individualTransfers[0].to.fspId = 'payer-dfsp'; + BackendRequests.__getBulkTransfers = jest.fn().mockReturnValue(Promise.resolve(backendResponse)); + + const model = new Model({ + ...config, + cache, + logger, + }); + + await model.getBulkTransfer(BULK_TRANSFER_ID, mockArgs.fspId); + + const call = MojaloopRequests.__putBulkTransfers.mock.calls[0]; + expect(call[0]).toEqual(BULK_TRANSFER_ID); + expect(call[1].bulkTransferState).toEqual('COMMITTED'); + const expectedResponse = {...getBulkTransfersMojaloopResponse}; + expectedResponse.individualTransferResults[0].fulfilment = undefined; + expect(call[1]).toMatchObject(expectedResponse); + }); + + test('getBulkTransfer should return not found error', async () => { + const BULK_TRANSFER_ID = 'fake-bulk-transfer-id'; + + BackendRequests.__getBulkTransfers = jest.fn().mockReturnValue( + Promise.reject(new HTTPResponseError({ + res: { + data: { + statusCode: '3208' + }, + } + }))); + + const model = new Model({ + ...config, + cache, + logger, + }); + + await model.getBulkTransfer(BULK_TRANSFER_ID, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkTransfersError).toHaveBeenCalledTimes(1); + const call = MojaloopRequests.__putBulkTransfersError.mock.calls[0]; + expect(call[0]).toEqual(`${BULK_TRANSFER_ID}`); + expect(call[1].errorInformation.errorCode).toEqual('3208'); + }); + + test('fail on bulk transfer without bulk quote.', async () => { + const BULK_TRANSFER_ID = 'without_bulk-quote-bulk-transfer-id'; + const args = { + bulkTransferId: BULK_TRANSFER_ID, + ilpPacket: 'mockBase64encodedIlpPacket', + condition: 'mockGeneratedCondition', + individualTransfers: [ + { + amount: { + currency: 'USD', + amount: 20.13 + }, + } + ] + }; + + const model = new Model({ + ...config, + cache, + logger, + allowTransferWithoutQuote: false, + }); + + await model.prepareBulkTransfer(args, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkTransfersError).toHaveBeenCalledTimes(1); + const call = MojaloopRequests.__putBulkTransfersError.mock.calls[0]; + expect(call[0]).toEqual(BULK_TRANSFER_ID); + expect(call[1].errorInformation.errorCode).toEqual('2001'); + }); + + test('pass on bulk transfer without bulk quote.', async () => { + const BULK_TRANSFER_ID = 'without_bulk-quote-bulk-transfer-id'; + const args = { + bulkTransferId: BULK_TRANSFER_ID, + individualTransfers: [ + { + transferId: 'fake-transfer-id', + transferAmount: { + currency: 'USD', + amount: 20.13 + }, + ilpPacket: 'mockBase64encodedIlpPacket', + condition: 'mockGeneratedCondition', + } + ] + }; + + const model = new Model({ + ...config, + cache, + logger, + allowTransferWithoutQuote: true, + rejectTransfersOnExpiredQuotes: false, + }); + + await model.prepareBulkTransfer(args, mockArgs.fspId); + + expect(MojaloopRequests.__putBulkTransfersError).toHaveBeenCalledTimes(0); + expect(BackendRequests.__postBulkTransfers).toHaveBeenCalledTimes(1); + expect(MojaloopRequests.__putBulkTransfers).toHaveBeenCalledTimes(1); + }); + }); + + describe('sendNotificationToPayee:', () => { + const transferId = '1234'; + let cache; + + beforeEach(async () => { + cache = new Cache({ + host: 'dummycachehost', + port: 1234, + logger, + }); + await cache.connect(); + }); + + afterEach(async () => { + await cache.disconnect(); + }); + + test('sends notification to fsp backend', async () => { + BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({})); + const backendResponse = JSON.parse(JSON.stringify(notificationToPayee)); + + const model = new Model({ + ...config, + cache, + logger, + }); + + await model.sendNotificationToPayee(backendResponse.data, transferId); + expect(BackendRequests.__putTransfersNotification).toHaveBeenCalledTimes(1); + const call = BackendRequests.__putTransfersNotification.mock.calls[0]; + expect(call[0]).toEqual(backendResponse.data); + expect(call[1]).toEqual(transferId); + }); + }); }); diff --git a/src/test/unit/lib/model/OutboundAuthorizationsModel.test.js b/src/test/unit/lib/model/OutboundAuthorizationsModel.test.js new file mode 100644 index 000000000..008d6670e --- /dev/null +++ b/src/test/unit/lib/model/OutboundAuthorizationsModel.test.js @@ -0,0 +1,305 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; + +// we use a mock standard components lib to intercept and mock certain funcs +jest.mock('@mojaloop/sdk-standard-components'); + +const { uuid } = require('uuidv4'); +const Model = require('@internal/model').OutboundAuthorizationsModel; +const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const defaultConfig = require('./data/defaultConfig'); +const mockLogger = require('../../mockLogger'); + + +describe('authorizationsModel', () => { + let cacheKey; + let data; + let modelConfig; + + const subId = 123; + let handler = null; + + afterEach(() => { + MojaloopRequests.__postAuthorizations = jest.fn(() => Promise.resolve()); + }); + + beforeEach(async () => { + modelConfig = { + logger: mockLogger({app: 'OutboundAuthorizationsModel-test'}), + + // there is no need to mock redis but only Cache + cache: { + get: jest.fn(() => Promise.resolve(data)), + set: jest.fn(() => Promise.resolve), + + // mock subscription and store handler + subscribe: jest.fn(async (channel, h) => { + handler = jest.fn(h); + return subId; + }), + + // mock publish and call stored handler + publish: jest.fn(async (channel, message) => await handler(channel, message, subId)), + + unsubscribe: jest.fn(() => Promise.resolve()) + }, + ...defaultConfig + }; + data = {the: 'mocked data', toParticipantId: 'pisp'}; + }); + + describe('create', () => { + test('proper creation of model', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + expect(model.state).toBe('start'); + + // model's methods layout + const methods = [ + 'run', 'getResponse', + 'onRequestAuthorization' + ]; + + methods.forEach((method) => expect(typeof model[method]).toEqual('function')); + }); + }); + + describe('loadFromCache', () => { + it('should load properly', async () => { + modelConfig.cache.get = jest.fn(() => Promise.resolve({source: 'yes I came from the cache'})); + + const model = await Model.loadFromCache(cacheKey, modelConfig); + expect(model.context.data.source).toEqual('yes I came from the cache'); + expect(model.context.cache.get).toBeCalledTimes(1); + expect(model.context.cache.get).toBeCalledWith(cacheKey); + }); + }); + + describe('getResponse', () => { + + it('should remap currentState', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + const states = model.allStates(); + + // should remap for all states except 'init' and 'none' + states.filter((s) => s !== 'init' && s !== 'none').forEach((state) => { + model.context.data.currentState = state; + const result = model.getResponse(); + expect(result.currentState).toEqual(Model.mapCurrentState[state]); + }); + + }); + + it('should handle unexpected state', async() => { + const model = await Model.create(data, cacheKey, modelConfig); + + // simulate lack of state by undefined property + delete model.context.data.currentState; + + const resp = model.getResponse(); + expect(resp.currentState).toEqual(Model.mapCurrentState.errored); + + // ensure that we log the problem properly + expect(modelConfig.logger.log).toBeCalledWith(`Authorization model response being returned from an unexpected state: ${undefined}. Returning ERROR_OCCURRED state`); + }); + }); + + describe('notificationChannel', () => { + it('should validate input', () => { + const invalidIds = [ + null, + undefined, + '' + ]; + invalidIds.forEach((id) => { + const invocation = () => Model.notificationChannel(id); + expect(invocation).toThrow('OutboundAuthorizationsModel.notificationChannel: \'id\' parameter is required'); + }); + }); + + it('should generate proper channel name', () => { + const id = uuid(); + expect(Model.notificationChannel(id)).toEqual(`authorizations_${id}`); + }); + + }); + + describe('onRequestAuthorization', () => { + + it('should implement happy flow', async () => { + data.transactionRequestId = uuid(); + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + // mock workflow execution which is tested in separate case + model.run = jest.fn(() => Promise.resolve()); + + const message = { + data: { + Iam: 'the-body', + transactionRequestId: model.context.data.transactionRequestId + } + }; + + // manually invoke transition handler + model.onRequestAuthorization(); + + // ensure handler wasn't called before publishing the message + expect(handler).not.toBeCalled(); + + // ensure that cache.unsubscribe does not happened before fire the message + expect(cache.unsubscribe).not.toBeCalled(); + + // fire publication with given message + await cache.publish(channel, JSON.stringify(message)); + + // handler should be called only once + expect(handler).toBeCalledTimes(1); + + // subscribe should be called only once + expect(cache.subscribe).toBeCalledTimes(1); + + // subscribe should be done to proper notificationChannel + expect(cache.subscribe.mock.calls[0][0]).toEqual(channel); + + // check invocation of request.postAuthorizations + expect(MojaloopRequests.__postAuthorizations).toBeCalledWith(Model.buildPostAuthorizationsRequest(data, modelConfig), data.toParticipantId); + + // check that this.context.data is updated + expect(model.context.data).toEqual({ + Iam: 'the-body', + transactionRequestId: model.context.data.transactionRequestId, + + // current state will be updated by onAfterTransition which isn't called + // when manual invocation of transition handler happens + currentState: 'start' + }); + + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(channel, subId); + }); + + it('should unsubscribe from cache in case when error happens in workflow run', async () => { + data.transactionRequestId = uuid(); + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + // invoke transition handler + model.onRequestAuthorization().catch((err) => { + expect(err.message).toEqual('Unexpected token u in JSON at position 0'); + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(channel, subId); + }); + + // fire publication to channel with invalid message + // should throw the exception from JSON.parse + await cache.publish(channel, undefined); + + }); + + it('should unsubscribe from cache in case when error happens Mojaloop requests', async () => { + // simulate error + MojaloopRequests.__postAuthorizations = jest.fn(() => Promise.reject('postAuthorization failed')); + data.transactionRequestId = uuid(); + + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + let theError = null; + // invoke transition handler + try { + await model.onRequestAuthorization(); + throw new Error('this point should not be reached'); + } catch (error) { + theError = error; + } + expect(theError).toEqual('postAuthorization failed'); + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(channel, subId); + }); + + }); + + describe('run workflow', () => { + it('start', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.requestAuthorization = jest.fn(); + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'start'; + const result = await model.run(); + expect(result).toEqual({the: 'response'}); + expect(model.requestAuthorization).toBeCalledTimes(1); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log.mock.calls).toEqual([ + ['State machine transitioned \'init\': none -> start'], + [`Authorization requested for ${model.context.data.transactionRequestId}, currentState: start`], + ['Authorization completed successfully'] + ]); + }); + it('succeeded', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'succeeded'; + const result = await model.run({the: 'message'}); + + expect(result).toEqual({the: 'response'}); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log).toBeCalledWith('Authorization completed successfully'); + }); + + it('errored', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'errored'; + const result = await model.run({the: 'message'}); + + expect(result).toBeFalsy(); + expect(model.getResponse).not.toBeCalled(); + expect(model.context.logger.log).toBeCalledWith('State machine in errored state'); + }); + + it('should handle errors', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.requestAuthorization = jest.fn(() => { + const err = new Error('requestAuthorization failed'); + err.authorizationState = 'some'; + return Promise.reject(err); + }); + model.error = jest.fn(); + model.context.data.currentState = 'start'; + + let theError = null; + try { + await model.run(); + throw new Error('this point should not be reached'); + } catch(error) { + theError = error; + } + // check propagation of original error + expect(theError.message).toEqual('requestAuthorization failed'); + + // ensure we start transition to errored state + expect(model.error).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/test/unit/lib/model/OutboundBulkQuotesModel.test.js b/src/test/unit/lib/model/OutboundBulkQuotesModel.test.js new file mode 100644 index 000000000..4387999da --- /dev/null +++ b/src/test/unit/lib/model/OutboundBulkQuotesModel.test.js @@ -0,0 +1,254 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Steven Oderayi - steven.oderayi@modusbox.com * + **************************************************************************/ + +'use strict'; + +// we use a mock standard components lib to intercept and mock certain funcs +jest.mock('@mojaloop/sdk-standard-components'); +jest.mock('redis'); + +const Cache = require('@internal/cache'); +const Model = require('@internal/model').OutboundBulkQuotesModel; + +const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components'); +const StateMachine = require('javascript-state-machine'); + +const defaultConfig = require('./data/defaultConfig'); +const bulkQuoteRequest = require('./data/bulkQuoteRequest'); +const bulkQuoteResponseTemplate = require('./data/bulkQuoteResponse'); + +// util function to simulate a quote response subscription message on a cache client +const emitBulkQuoteResponseCacheMessage = (cache, bulkQuoteId, bulkQuoteResponse) => { + cache.publish(`bulkQuote_${bulkQuoteId}`, JSON.stringify(bulkQuoteResponse)); +}; + +describe('OutboundBulkQuotesModel', () => { + let bulkQuoteResponse; + let config; + let logger; + let cache; + + /** + * + * @param {Object} opts + * @param {Number} opts.expirySeconds + * @param {Object} opts.delays + * @param {Number} delays.requestBulkQuotes + * @param {Object} opts.rejects + * @param {boolean} rejects.bulkQuoteResponse + */ + async function testBulkQuoteWithDelay({expirySeconds, delays, rejects}) { + const config = JSON.parse(JSON.stringify(defaultConfig)); + config.expirySeconds = expirySeconds; + config.rejectExpiredQuoteResponses = rejects.bulkQuoteResponse; + + // simulate a delayed callback with the bulk quote response + MojaloopRequests.__postBulkQuotes = jest.fn((postBulkQuotesBody) => { + setTimeout(() => { + emitBulkQuoteResponseCacheMessage(cache, postBulkQuotesBody.bulkQuoteId, bulkQuoteResponse); + }, delays.requestBulkQuotes ? delays.requestBulkQuotes * 1000 : 0); + }); + + const model = new Model({ + ...config, + cache, + logger, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkQuoteRequest))); + + let expectError; + + if (rejects.bulkQuoteResponse && delays.requestBulkQuotes && expirySeconds < delays.requestBulkQuotes) { + expectError = 'Bulk quote response missed expiry deadline'; + } + + if (expectError) { + await expect(model.run()).rejects.toThrowError(expectError); + } else { + const result = await model.run(); + await expect(result.currentState).toBe('COMPLETED'); + } + } + + beforeAll(async () => { + logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' }); + bulkQuoteResponse = JSON.parse(JSON.stringify(bulkQuoteResponseTemplate)); + }); + + beforeEach(async () => { + config = JSON.parse(JSON.stringify(defaultConfig)); + + MojaloopRequests.__postBulkQuotes = jest.fn(() => Promise.resolve()); + MojaloopRequests.__putBulkQuotes = jest.fn(() => Promise.resolve()); + MojaloopRequests.__putBulkQuotesError = jest.fn(() => Promise.resolve()); + + cache = new Cache({ + host: 'dummycachehost', + port: 1234, + logger, + }); + await cache.connect(); + }); + + afterEach(async () => { + await cache.disconnect(); + }); + + test('initializes to starting state', async () => { + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkQuoteRequest))); + expect(StateMachine.__instance.state).toBe('start'); + }); + + test('test get bulk quote', async () => { + MojaloopRequests.__getBulkQuotes = jest.fn((bulkQuoteId) => { + emitBulkQuoteResponseCacheMessage(cache, bulkQuoteId, bulkQuoteResponse); + return Promise.resolve(); + }); + + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + const BULK_QUOTE_ID = 'bq-id000011'; + + await model.initialize(JSON.parse(JSON.stringify({ + currentState: 'getBulkQuote', + bulkQuoteId: BULK_QUOTE_ID, + }))); + + expect(StateMachine.__instance.state).toBe('getBulkQuote'); + + // start the model running + const result = await model.run(); + + expect(MojaloopRequests.__getBulkQuotes).toHaveBeenCalledTimes(1); + + // check we stopped at succeeded state + expect(result.currentState).toBe('COMPLETED'); + expect(StateMachine.__instance.state).toBe('succeeded'); + }); + + test('sends bulk quotes request with correct payload', async () => { + MojaloopRequests.__postBulkQuotes = jest.fn((postBulkQuotesBody) => { + // ensure that the `MojaloopRequests.postBulkQuotes` method has been called with correct arguments + // including extension list + const extensionList = postBulkQuotesBody.extensionList.extension; + expect(extensionList).toBeTruthy(); + expect(extensionList.length).toBe(2); + expect(extensionList[0]).toEqual({ key: 'qkey1', value: 'qvalue1' }); + expect(extensionList[1]).toEqual({ key: 'qkey2', value: 'qvalue2' }); + + // simulate a callback with the bulk quote response + emitBulkQuoteResponseCacheMessage(cache, postBulkQuotesBody.bulkQuoteId, bulkQuoteResponse); + return Promise.resolve(); + }); + + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkQuoteRequest))); + + expect(StateMachine.__instance.state).toBe('start'); + + // start the model running + const result = await model.run(); + + expect(MojaloopRequests.__postBulkQuotes).toHaveBeenCalledTimes(1); + + // check we stopped at 'succeeded' state + expect(result.currentState).toBe('COMPLETED'); + expect(StateMachine.__instance.state).toBe('succeeded'); + }); + + test('pass quote response `expiration` deadline', () => + testBulkQuoteWithDelay({ + expirySeconds: 2, + delays: { + requestBulkQuotes: 1, + }, + rejects: { + bulkQuoteResponse: true, + } + }) + ); + + test('fail on quote response `expiration` deadline', () => + testBulkQuoteWithDelay({ + expirySeconds: 1, + delays: { + requestBulkQuotes: 2, + }, + rejects: { + bulkQuoteResponse: true, + } + }) + ); + + test('Throws with mojaloop error in response body when quote request error callback occurs', async () => { + const expectError = { + type: 'bulkQuoteResponseError', + data: { + errorInformation: { + errorCode: '3205', + errorDescription: 'Bulk quote ID not found' + } + } + }; + + MojaloopRequests.__postBulkQuotes = jest.fn((postBulkQuotesBody) => { + // simulate a callback with the bulk quote response + cache.publish(`bulkQuote_${postBulkQuotesBody.bulkQuoteId}`, JSON.stringify(expectError)); + return Promise.resolve(); + }); + + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkQuoteRequest))); + + expect(StateMachine.__instance.state).toBe('start'); + + const errMsg = 'Got an error response requesting bulk quote: { errorInformation:\n { errorCode: \'3205\', errorDescription: \'Bulk quote ID not found\' } }'; + + try { + await model.run(); + } + catch(err) { + expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,'')); + expect(err.bulkQuoteState).toBeTruthy(); + expect(err.bulkQuoteState.lastError).toBeTruthy(); + expect(err.bulkQuoteState.lastError.mojaloopError).toEqual(expectError.data); + expect(err.bulkQuoteState.lastError.bulkQuoteState).toBe(undefined); + return; + } + + throw new Error('Outbound bulk quotes model should have thrown'); + }); +}); diff --git a/src/test/unit/lib/model/OutboundBulkTransfersModel.test.js b/src/test/unit/lib/model/OutboundBulkTransfersModel.test.js new file mode 100644 index 000000000..8c6e00358 --- /dev/null +++ b/src/test/unit/lib/model/OutboundBulkTransfersModel.test.js @@ -0,0 +1,249 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Steven Oderayi - steven.oderayi@modusbox.com * + **************************************************************************/ + +'use strict'; + +// we use a mock standard components lib to intercept and mock certain funcs +jest.mock('@mojaloop/sdk-standard-components'); +jest.mock('redis'); + +const Cache = require('@internal/cache'); +const Model = require('@internal/model').OutboundBulkTransfersModel; + +const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components'); +const StateMachine = require('javascript-state-machine'); + +const defaultConfig = require('./data/defaultConfig'); +const bulkTransferRequest = require('./data/bulkTransferRequest'); +const bulkTransferFulfil = require('./data/bulkTransferFulfil'); + +// util function to simulate a bulk transfer fulfilment subscription message on a cache client +const emitBulkTransferFulfilCacheMessage = (cache, bulkTransferId, fulfils) => cache.publish(`bulkTransfer_${bulkTransferId}`, JSON.stringify(fulfils)); + +describe('outboundBulkTransferModel', () => { + let config; + let logger; + let cache; + + /** + * + * @param {Object} opts + * @param {Number} opts.expirySeconds + * @param {Object} opts.delays + * @param {Number} delays.prepareTransfer + * @param {Object} opts.rejects + * @param {boolean} rejects.transferFulfils + */ + async function testBulkTransferWithDelay({expirySeconds, delays, rejects}) { + const config = JSON.parse(JSON.stringify(defaultConfig)); + config.expirySeconds = expirySeconds; + config.rejectExpiredTransferFulfils = rejects.transferFulfils; + + // simulate a delayed callback with the bulk transfer fulfilments + MojaloopRequests.__postBulkTransfers = jest.fn((postBulkTransfersBody) => { + setTimeout(() => { + emitBulkTransferFulfilCacheMessage(cache, postBulkTransfersBody.bulkTransferId, bulkTransferFulfil); + }, delays.prepareTransfer ? delays.prepareTransfer * 1000 : 0); + }); + + const model = new Model({ + ...config, + cache, + logger, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkTransferRequest))); + + let expectError; + + if (rejects.transferFulfils && delays.prepareTransfer && expirySeconds < delays.prepareTransfer) { + expectError = 'Bulk transfer fulfils missed expiry deadline'; + } + if (expectError) { + await expect(model.run()).rejects.toThrowError(expectError); + } else { + const result = await model.run(); + await expect(result.currentState).toBe('COMPLETED'); + } + } + + beforeAll(async () => { + logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' }); + }); + + beforeEach(async () => { + config = JSON.parse(JSON.stringify(defaultConfig)); + MojaloopRequests.__postBulkTransfers = jest.fn(() => Promise.resolve()); + + cache = new Cache({ + host: 'dummycachehost', + port: 1234, + logger, + }); + await cache.connect(); + }); + + afterEach(async () => { + await cache.disconnect(); + }); + + test('initializes to starting state', async () => { + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkTransferRequest))); + expect(StateMachine.__instance.state).toBe('start'); + }); + + + test('executes bulk transfer', async () => { + MojaloopRequests.__postBulkTransfers = jest.fn((postBulkTransfersBody) => { + //ensure that the `MojaloopRequests.postBulkTransfers` method has been called with the correct arguments + // set as the destination FSPID + const extensionList = postBulkTransfersBody.extensionList.extension; + expect(extensionList).toBeTruthy(); + expect(extensionList.length).toBe(2); + expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' }); + expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' }); + + // simulate a callback with the transfer fulfilment + emitBulkTransferFulfilCacheMessage(cache, postBulkTransfersBody.bulkTransferId, bulkTransferFulfil); + return Promise.resolve(); + }); + + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkTransferRequest))); + + expect(StateMachine.__instance.state).toBe('start'); + + // start the model running + const result = await model.run(); + + expect(MojaloopRequests.__postBulkTransfers).toHaveBeenCalledTimes(1); + + // check we stopped at succeeded state + expect(result.currentState).toBe('COMPLETED'); + expect(StateMachine.__instance.state).toBe('succeeded'); + }); + + test('test get bulk transfer', async () => { + MojaloopRequests.__getBulkTransfers = jest.fn((bulkTransferId) => { + emitBulkTransferFulfilCacheMessage(cache, bulkTransferId, bulkTransferFulfil); + return Promise.resolve(); + }); + + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + const BULK_TRANSFER_ID = 'btx-id000011'; + + await model.initialize(JSON.parse(JSON.stringify({ + ...bulkTransferRequest, + currentState: 'getBulkTransfer', + bulkTransferId: BULK_TRANSFER_ID, + }))); + + expect(StateMachine.__instance.state).toBe('getBulkTransfer'); + + // start the model running + const result = await model.run(); + + expect(MojaloopRequests.__getBulkTransfers).toHaveBeenCalledTimes(1); + + // check we stopped at succeeded state + expect(result.currentState).toBe('COMPLETED'); + expect(StateMachine.__instance.state).toBe('succeeded'); + }); + + test('pass transfer fulfills `expiration` deadline', () => + testBulkTransferWithDelay({ + expirySeconds: 2, + delays: { + prepareTransfer: 1, + }, + rejects: { + transferFulfils: true, + } + }) + ); + + test('fail on transfer fulfills `expiration` deadline', () => + testBulkTransferWithDelay({ + expirySeconds: 1, + delays: { + prepareTransfer: 2, + }, + rejects: { + transferFulfils: true, + } + }) + ); + + + test('Throws with mojaloop error in response body when transfer request error callback occurs', async () => { + const expectError = { + type: 'bulkTransferError', + data: { + errorInformation: { + errorCode: '4001', + errorDescription: 'Payer FSP insufficient liquidity' + } + } + }; + + MojaloopRequests.__postBulkTransfers = jest.fn((postBulkTransfersBody) => { + // simulate an error callback with the transfer fulfilments + cache.publish(`bulkTransfer_${postBulkTransfersBody.bulkTransferId}`, JSON.stringify(expectError)); + return Promise.resolve(); + }); + + const model = new Model({ + cache, + logger, + ...config, + tls: config.outbound.tls, + }); + + await model.initialize(JSON.parse(JSON.stringify(bulkTransferRequest))); + + expect(StateMachine.__instance.state).toBe('start'); + + const errMsg = 'Got an error response preparing bulk transfer: { errorInformation:\n { errorCode: \'4001\',\n errorDescription: \'Payer FSP insufficient liquidity\' } }'; + + try { + await model.run(); + } + catch(err) { + expect(err.message.replace(/[ \n]/g,'')).toEqual(errMsg.replace(/[ \n]/g,'')); + expect(err.bulkTransferState).toBeTruthy(); + expect(err.bulkTransferState.lastError).toBeTruthy(); + expect(err.bulkTransferState.lastError.mojaloopError).toEqual(expectError.data); + expect(err.bulkTransferState.lastError.bulkTransferState).toBe(undefined); + return; + } + + throw new Error('Outbound model should have thrown'); + }); +}); diff --git a/src/test/unit/lib/model/OutboundRequestToPayModel.test.js b/src/test/unit/lib/model/OutboundRequestToPayModel.test.js index 55a2f2b3f..8fb8a43de 100644 --- a/src/test/unit/lib/model/OutboundRequestToPayModel.test.js +++ b/src/test/unit/lib/model/OutboundRequestToPayModel.test.js @@ -14,12 +14,10 @@ jest.mock('@mojaloop/sdk-standard-components'); jest.mock('redis'); -const util = require('util'); const Cache = require('@internal/cache'); const Model = require('@internal/model').OutboundRequestToPayModel; -const { Logger, Transports } = require('@internal/log'); -const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components'); const StateMachine = require('javascript-state-machine'); const defaultConfig = require('./data/defaultConfig'); @@ -55,10 +53,9 @@ describe('outboundModel', () => { * @param {boolean} rejects.quoteResponse * @param {boolean} rejects.transferFulfils */ - + beforeAll(async () => { - const logTransports = await Promise.all([Transports.consoleDir()]); - logger = new Logger({ context: { app: 'outbound-model-unit-tests-cache' }, space: 4, transports: logTransports }); + logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' }); transactionRequestResponse = JSON.parse(JSON.stringify(transactionRequestResponseTemplate)); }); @@ -67,7 +64,7 @@ describe('outboundModel', () => { MojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve()); MojaloopRequests.__getParties = jest.fn(() => Promise.resolve()); MojaloopRequests.__postTransactionRequests = jest.fn(() => Promise.resolve()); - + cache = new Cache({ host: 'dummycachehost', port: 1234, @@ -85,6 +82,7 @@ describe('outboundModel', () => { cache, logger, ...config, + tls: config.outbound.tls, }); await model.initialize(JSON.parse(JSON.stringify(requestToPayRequest))); @@ -94,7 +92,7 @@ describe('outboundModel', () => { test('executes all two stages without halting when AUTO_ACCEPT_PARTY is true', async () => { config.autoAcceptParty = true; - + MojaloopRequests.__getParties = jest.fn(() => { emitPartyCacheMessage(cache, payeeParty); return Promise.resolve(); @@ -110,6 +108,7 @@ describe('outboundModel', () => { cache, logger, ...config, + tls: config.outbound.tls, }); await model.initialize(JSON.parse(JSON.stringify(requestToPayRequest))); @@ -119,11 +118,9 @@ describe('outboundModel', () => { // start the model running const result = await model.run(); - console.log(`Result after two stage transfer: ${util.inspect(result)}`); - expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__postTransactionRequests).toHaveBeenCalledTimes(1); - + // check we stopped at payeeResolved state expect(result.currentState).toBe('COMPLETED'); expect(result.requestToPayState).toBe('RECEIVED'); @@ -137,11 +134,12 @@ describe('outboundModel', () => { emitPartyCacheMessage(cache, payeeParty); return Promise.resolve(); }); - + const model = new Model({ cache, logger, ...config, + tls: config.outbound.tls, }); await model.initialize(JSON.parse(JSON.stringify(requestToPayRequest))); @@ -157,12 +155,10 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state const result = await resultPromise; - console.log(`Result after resolve payee: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE'); expect(StateMachine.__instance.state).toBe('payeeResolved'); }); - - + + }); diff --git a/src/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js b/src/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js index 78f771233..81384a2dd 100644 --- a/src/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +++ b/src/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js @@ -14,12 +14,10 @@ jest.mock('@mojaloop/sdk-standard-components'); jest.mock('redis'); -const util = require('util'); const Cache = require('@internal/cache'); const Model = require('@internal/model').OutboundRequestToPayTransferModel; -const { Logger, Transports } = require('@internal/log'); -const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components'); const StateMachine = require('javascript-state-machine'); const defaultConfig = require('./data/defaultConfig'); @@ -55,11 +53,10 @@ describe('outboundRequestToPayTransferModel', () => { * @param {boolean} rejects.quoteResponse * @param {boolean} rejects.transferFulfils */ - + beforeAll(async () => { - const logTransports = await Promise.all([Transports.consoleDir()]); - logger = new Logger({ context: { app: 'outbound-model-unit-tests-cache' }, space: 4, transports: logTransports }); + logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' }); quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate)); }); @@ -90,6 +87,7 @@ describe('outboundRequestToPayTransferModel', () => { cache, logger, ...config, + tls: config.outbound.tls, }); await model.initialize(JSON.parse(JSON.stringify(requestToPayTransferRequest))); @@ -144,6 +142,7 @@ describe('outboundRequestToPayTransferModel', () => { cache, logger, ...config, + tls: config.outbound.tls, }); await model.initialize(JSON.parse(JSON.stringify(requestToPayTransferRequest))); @@ -153,8 +152,6 @@ describe('outboundRequestToPayTransferModel', () => { // start the model running const result = await model.run(); - console.log(`Result after three stage transfer: ${util.inspect(result)}`); - expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__getAuthorizations).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1); @@ -165,7 +162,7 @@ describe('outboundRequestToPayTransferModel', () => { }); // test('halts and resumes after quotes and otp stages when AUTO_ACCEPT_QUOTES is false and AUTO_ACCEPT_OTP is false', async () => { - + // config.autoAcceptR2PDeviceOTP = false; // config.autoAcceptR2PDeviceQuotes = false; @@ -173,6 +170,7 @@ describe('outboundRequestToPayTransferModel', () => { // cache, // logger, // ...config, + // tls: config.outbound.tls, // }); // await model.initialize(JSON.parse(JSON.stringify(requestToPayTransferRequest))); @@ -188,8 +186,6 @@ describe('outboundRequestToPayTransferModel', () => { // // wait for the model to reach a terminal state // let result = await resultPromise; - // console.log(`Result after request quote: ${util.inspect(result)}`); - // // check we stopped at quoteReceived state // expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE'); // expect(StateMachine.__instance.state).toBe('quoteReceived'); @@ -201,6 +197,7 @@ describe('outboundRequestToPayTransferModel', () => { // cache, // logger, // ...config, + // tls: config.outbound.tls, // }); // await model.load(requestToPayTransactionId); @@ -217,8 +214,6 @@ describe('outboundRequestToPayTransferModel', () => { // // wait for the model to reach a terminal state // result = await resultPromise; - // console.log(`Result after request otp: ${util.inspect(result)}`); - // // check we stopped at quoteReceived state // expect(result.currentState).toBe('WAITING_FOR_OTP_ACCEPTANCE'); // expect(StateMachine.__instance.state).toBe('otpReceived'); @@ -228,6 +223,7 @@ describe('outboundRequestToPayTransferModel', () => { // cache, // logger, // ...config, + // tls: config.outbound.tls, // }); // await model.load(requestToPayTransactionId); @@ -244,13 +240,11 @@ describe('outboundRequestToPayTransferModel', () => { // // wait for the model to reach a terminal state // result = await resultPromise; - // console.log(`Result after transfer fulfil: ${util.inspect(result)}`); - // // check we stopped at quoteReceived state // expect(result.currentState).toBe('COMPLETED'); // expect(StateMachine.__instance.state).toBe('succeeded'); // }); - + }); diff --git a/src/test/unit/lib/model/OutboundThirdpartyTransactionModel.test.js b/src/test/unit/lib/model/OutboundThirdpartyTransactionModel.test.js new file mode 100644 index 000000000..aecf9eabd --- /dev/null +++ b/src/test/unit/lib/model/OutboundThirdpartyTransactionModel.test.js @@ -0,0 +1,535 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Kevin Leyow - kevin.leyow@modusbox.com * + **************************************************************************/ + +'use strict'; + +// we use a mock standard components lib to intercept and mock certain funcs +jest.mock('@mojaloop/sdk-standard-components'); + +const { uuid } = require('uuidv4'); +const Model = require('@internal/model').OutboundThirdpartyTransactionModel; +const { ThirdpartyRequests } = require('@mojaloop/sdk-standard-components'); +const defaultConfig = require('./data/defaultConfig'); +const mockLogger = require('../../mockLogger'); + + +describe('thirdpartyTransactionModel', () => { + let cacheKey; + let data; + let modelConfig; + + const subId = 123; + let handler = null; + + afterEach(() => { + ThirdpartyRequests.__getThirdpartyRequestsTransactions = jest.fn(() => Promise.resolve()); + }); + + /** + * + * @param {Object} opts + * @param {Number} opts.expirySeconds + * @param {Object} opts.delays + * @param {Number} delays.requestQuotes + * @param {Number} delays.prepareTransfer + * @param {Object} opts.rejects + * @param {boolean} rejects.quoteResponse + * @param {boolean} rejects.transferFulfils + */ + + + beforeEach(async () => { + modelConfig = { + logger: mockLogger({app: 'OutboundThirdpartyTransactionModel-test'}), + + // there is no need to mock redis but only Cache + cache: { + get: jest.fn(() => Promise.resolve(data)), + set: jest.fn(() => Promise.resolve), + + // mock subscription and store handler + subscribe: jest.fn(async (channel, h) => { + handler = jest.fn(h); + return subId; + }), + + // mock publish and call stored handler + publish: jest.fn(async (channel, message) => await handler(channel, message, subId)), + + unsubscribe: jest.fn(() => Promise.resolve()) + }, + ...defaultConfig + }; + data = { + currentState: 'getTransaction', + }; + }); + + describe('create', () => { + test('proper creation of model', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + expect(model.state).toBe('getTransaction'); + + // model's methods layout + const methods = [ + 'run', 'getResponse', + 'onGetThirdPartyTransaction', + 'onPostThirdPartyTransaction' + ]; + + methods.forEach((method) => expect(typeof model[method]).toEqual('function')); + }); + }); + + describe('loadFromCache', () => { + it('should load properly', async () => { + modelConfig.cache.get = jest.fn(() => Promise.resolve({source: 'yes I came from the cache'})); + + const model = await Model.loadFromCache(cacheKey, modelConfig); + expect(model.context.data.source).toEqual('yes I came from the cache'); + expect(model.context.cache.get).toBeCalledTimes(1); + expect(model.context.cache.get).toBeCalledWith(cacheKey); + }); + }); + + describe('getResponse', () => { + + it('should remap currentState', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + const states = model.allStates(); + + // should remap for all states except 'init' and 'none' + states.filter((s) => s !== 'init' && s !== 'none').forEach((state) => { + model.context.data.currentState = state; + const result = model.getResponse(); + expect(result.currentState).toEqual(Model.mapCurrentState[state]); + }); + + }); + + it('should handle unexpected state', async() => { + const model = await Model.create(data, cacheKey, modelConfig); + + // simulate lack of state by undefined property + delete model.context.data.currentState; + + const resp = model.getResponse(); + expect(resp.currentState).toEqual(Model.mapCurrentState.errored); + + // ensure that we log the problem properly + expect(modelConfig.logger.log).toBeCalledWith(`OutboundThirdpartyTransaction model response being returned from an unexpected state: ${undefined}. Returning ERROR_OCCURRED state`); + }); + }); + + describe('notificationChannel', () => { + it('should validate input', () => { + const invalidIds = [ + null, + undefined, + '' + ]; + invalidIds.forEach((id) => { + const invocation = () => Model.notificationChannel(id); + expect(invocation).toThrow('OutboundThirdpartyTransactionModel.notificationChannel: \'id\' parameter is required'); + }); + }); + + it('should generate proper channel name', () => { + const id = uuid(); + expect(Model.notificationChannel(id)).toEqual(`3ptrxnreq_${id}`); + }); + + }); + + describe('onGetThirdPartyTransaction', () => { + + it('should implement happy flow', async () => { + data.transactionRequestId = uuid(); + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + // mock workflow execution which is tested in separate case + model.run = jest.fn(() => Promise.resolve()); + + const message = { + data: { + 'transactionId': '5a2ad5dc-4ab1-4a22-8c5b-62f75252a8d5', + 'transactionRequestState': 'RECEIVED' + } + }; + + // manually invoke transition handler + model.onGetThirdPartyTransaction(); + + // ensure handler wasn't called before publishing the message + expect(handler).not.toBeCalled(); + + // ensure that cache.unsubscribe does not happened before fire the message + expect(cache.unsubscribe).not.toBeCalled(); + + + // fire publication with given message + await cache.publish(channel, JSON.stringify(message)); + + // handler should be called only once + expect(handler).toBeCalledTimes(1); + + // subscribe should be called only once + expect(cache.subscribe).toBeCalledTimes(1); + + // subscribe should be done to proper notificationChannel + expect(cache.subscribe.mock.calls[0][0]).toEqual(channel); + + // check invocation of request.getThirdpartyRequestsTransactions + expect(ThirdpartyRequests.__getThirdpartyRequestsTransactions).toBeCalledWith(data.transactionRequestId, null); + + // check that this.context.data is updated + expect(model.context.data).toEqual({ + 'transactionId': '5a2ad5dc-4ab1-4a22-8c5b-62f75252a8d5', + 'transactionRequestState': 'RECEIVED', + + // current state will be updated by onAfterTransition which isn't called + // when manual invocation of transition handler happens + currentState: 'getTransaction' + }); + + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(subId); + }); + + it('should unsubscribe from cache in case when error happens in workflow run', async () => { + data.transactionRequestId = uuid(); + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + // invoke transition handler + model.onGetThirdPartyTransaction().catch((err) => { + expect(err.message).toEqual('Unexpected token u in JSON at position 0'); + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(subId); + }); + + // fire publication to channel with invalid message + // should throw the exception from JSON.parse + await cache.publish(channel, undefined); + + }); + + it('should unsubscribe from cache in case when error happens Mojaloop requests', async () => { + // simulate error + ThirdpartyRequests.__getThirdpartyRequestsTransactions = jest.fn(() => Promise.reject('getThirdPartyTransaction failed')); + data.transactionRequestId = uuid(); + + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + let theError = null; + // invoke transition handler + try { + await model.onGetThirdPartyTransaction(); + throw new Error('this point should not be reached'); + } catch (error) { + theError = error; + } + expect(theError).toEqual('getThirdPartyTransaction failed'); + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(subId); + }); + }); + + describe('onPostThirdPartyTransaction', () => { + + it('should implement happy flow', async () => { + data.transactionRequestId = uuid(); + data.currentState = 'postTransaction'; + let payerInformation = { + 'personalInfo': { + 'complexName': { + 'firstName': 'Ayesha', + 'lastName': 'Takia' + } + }, + 'partyIdInfo': { + 'partyIdType': 'MSISDN', + 'partyIdentifier': '+44 8765 4321', + 'fspId': 'dfspa' + } + }; + data.payer = payerInformation; + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + // mock workflow execution which is tested in separate case + model.run = jest.fn(() => Promise.resolve()); + + const message = { + data: { + 'transactionId': '5a2ad5dc-4ab1-4a22-8c5b-62f75252a8d5', + 'transactionRequestState': 'RECEIVED' + } + }; + + // manually invoke transition handler + model.onPostThirdPartyTransaction() + .then(() => { + // subscribe should be called only once + expect(cache.subscribe).toBeCalledTimes(1); + + // subscribe should be done to proper notificationChannel + expect(cache.subscribe.mock.calls[0][0]).toEqual(channel); + + // check invocation of request.postThirdpartyRequestsTransactions + expect(ThirdpartyRequests.__postThirdpartyRequestsTransactions).toBeCalledWith(data, 'dfspa'); + + // check that this.context.data is updated + expect(model.context.data).toEqual({ + 'transactionId': '5a2ad5dc-4ab1-4a22-8c5b-62f75252a8d5', + 'transactionRequestState': 'RECEIVED', + // current state will be updated by onAfterTransition which isn't called + // when manual invocation of transition handler happens + currentState: 'postTransaction' + }); + }); + + // ensure handler wasn't called before publishing the message + expect(handler).not.toBeCalled(); + + // ensure that cache.unsubscribe does not happened before fire the message + expect(cache.unsubscribe).not.toBeCalled(); + + + // fire publication with given message + await cache.publish(channel, JSON.stringify(message)); + + // handler should be called only once + expect(handler).toBeCalledTimes(1); + + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(subId); + }); + + it('should unsubscribe from cache in case when error happens in workflow run', async () => { + data.transactionRequestId = uuid(); + data.currentState = 'postTransaction'; + let payerInformation = { + 'personalInfo': { + 'complexName': { + 'firstName': 'Ayesha', + 'lastName': 'Takia' + } + }, + 'partyIdInfo': { + 'partyIdType': 'MSISDN', + 'partyIdentifier': '+44 8765 4321', + 'fspId': 'dfspa' + } + }; + data.payer = payerInformation; + const channel = Model.notificationChannel(data.transactionRequestId); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + // invoke transition handler + model.onPostThirdPartyTransaction().catch((err) => { + expect(err.message).toEqual('Unexpected token u in JSON at position 0'); + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(subId); + }); + + // fire publication to channel with invalid message + // should throw the exception from JSON.parse + await cache.publish(channel, undefined); + + }); + + it('should unsubscribe from cache in case when error happens Mojaloop requests', async () => { + // simulate error + ThirdpartyRequests.__postThirdpartyRequestsTransactions = jest.fn(() => Promise.reject('postThirdPartyTransaction failed')); + data.transactionRequestId = uuid(); + let payerInformation = { + 'personalInfo': { + 'complexName': { + 'firstName': 'Ayesha', + 'lastName': 'Takia' + } + }, + 'partyIdInfo': { + 'partyIdType': 'MSISDN', + 'partyIdentifier': '+44 8765 4321', + 'fspId': 'dfspa' + } + }; + data.payer = payerInformation; + + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + let theError = null; + // invoke transition handler + try { + await model.onPostThirdPartyTransaction(); + throw new Error('this point should not be reached'); + } catch (error) { + theError = error; + } + expect(theError).toEqual('postThirdPartyTransaction failed'); + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(subId); + }); + }); + + describe('run get thirdparty transaction workflow', () => { + + it('start', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.onGetThirdPartyTransaction = jest.fn(() => Promise.resolve({the: 'response'})); + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + const result = await model.run(); + expect(result).toEqual({the: 'response'}); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log.mock.calls).toEqual([ + ['State machine transitioned \'init\': none -> getTransaction'], + ['State machine transitioned \'getThirdPartyTransaction\': getTransaction -> transactionSuccess'], + [`GET Thirdparty transaction requested for ${data.transactionRequestId}, currentState: ${data.currentState}`], + ['Thirdparty request model state machine transition completed in state: transactionSuccess. Recursing to handle next transition.'], + ['ThirdpartyTransaction completed successfully'] + ]); + }); + + it('transactionSuccess', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'transactionSuccess'; + const result = await model.run(); + + expect(result).toEqual({the: 'response'}); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log).toBeCalledWith('ThirdpartyTransaction completed successfully'); + }); + + it('errored', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'errored'; + const result = await model.run(); + + expect(result).toBeFalsy(); + expect(model.getResponse).not.toBeCalled(); + expect(model.context.logger.log).toBeCalledWith('State machine in errored state'); + }); + + it('should handle errors', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getThirdPartyTransaction = jest.fn(() => { + const err = new Error('getTransaction failed'); + err.authorizationState = 'some'; + return Promise.reject(err); + }); + model.error = jest.fn(); + + let theError = null; + try { + await model.run(); + throw new Error('this point should not be reached'); + } catch(error) { + theError = error; + } + // check propagation of original error + expect(theError.message).toEqual('getTransaction failed'); + + // ensure we start transition to errored state + expect(model.error).toBeCalledTimes(1); + }); + }); + + describe('run post thirdparty transaction workflow', () => { + + it('start', async () => { + data.currentState = 'postTransaction'; + const model = await Model.create(data, cacheKey, modelConfig); + + model.onPostThirdPartyTransaction = jest.fn(() => Promise.resolve({the: 'response'})); + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + const result = await model.run(); + expect(result).toEqual({the: 'response'}); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log.mock.calls).toEqual([ + ['State machine transitioned \'init\': none -> postTransaction'], + ['State machine transitioned \'postThirdPartyTransaction\': postTransaction -> transactionSuccess'], + [`POST Thirdparty transaction requested for ${data.transactionRequestId}, currentState: ${data.currentState}`], + ['Thirdparty request model state machine transition completed in state: transactionSuccess. Recursing to handle next transition.'], + ['ThirdpartyTransaction completed successfully'] + ]); + }); + + it('transactionSuccess', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'transactionSuccess'; + const result = await model.run(); + + expect(result).toEqual({the: 'response'}); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log).toBeCalledWith('ThirdpartyTransaction completed successfully'); + }); + + it('errored', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'errored'; + const result = await model.run(); + + expect(result).toBeFalsy(); + expect(model.getResponse).not.toBeCalled(); + expect(model.context.logger.log).toBeCalledWith('State machine in errored state'); + }); + + it('should handle errors', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + model.getThirdPartyTransaction = jest.fn(() => { + const err = new Error('postTransaction failed'); + err.authorizationState = 'some'; + return Promise.reject(err); + }); + model.error = jest.fn(); + + let theError = null; + try { + await model.run(); + throw new Error('this point should not be reached'); + } catch(error) { + theError = error; + } + // check propagation of original error + expect(theError.message).toEqual('postTransaction failed'); + + // ensure we start transition to errored state + expect(model.error).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/test/unit/lib/model/OutboundTransfersModel.test.js b/src/test/unit/lib/model/OutboundTransfersModel.test.js index 6a1f6192f..3d197bdd3 100644 --- a/src/test/unit/lib/model/OutboundTransfersModel.test.js +++ b/src/test/unit/lib/model/OutboundTransfersModel.test.js @@ -14,12 +14,10 @@ jest.mock('@mojaloop/sdk-standard-components'); jest.mock('redis'); -const util = require('util'); const Cache = require('@internal/cache'); const Model = require('@internal/model').OutboundTransfersModel; -const { Logger, Transports } = require('@internal/log'); -const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const { MojaloopRequests, Logger } = require('@mojaloop/sdk-standard-components'); const StateMachine = require('javascript-state-machine'); const defaultConfig = require('./data/defaultConfig'); @@ -108,8 +106,7 @@ describe('outboundModel', () => { } beforeAll(async () => { - const logTransports = await Promise.all([Transports.consoleDir()]); - logger = new Logger({ context: { app: 'outbound-model-unit-tests-cache' }, space: 4, transports: logTransports }); + logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' }); quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate)); }); @@ -202,8 +199,6 @@ describe('outboundModel', () => { // start the model running const result = await model.run(); - console.log(`Result after three stage transfer: ${util.inspect(result)}`); - expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1); @@ -284,8 +279,6 @@ describe('outboundModel', () => { // start the model running const result = await model.run(); - console.log(`Result after three stage transfer: ${util.inspect(result)}`); - expect(MojaloopRequests.__getParties).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__postQuotes).toHaveBeenCalledTimes(1); expect(MojaloopRequests.__postTransfers).toHaveBeenCalledTimes(1); @@ -321,8 +314,6 @@ describe('outboundModel', () => { // start the model running const result = await model.run(); - console.log(`Result after get transfer: ${util.inspect(result)}`); - expect(MojaloopRequests.__getTransfers).toHaveBeenCalledTimes(1); // check we stopped at payeeResolved state @@ -353,8 +344,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state const result = await resultPromise; - console.log(`Result after resolve payee: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE'); expect(StateMachine.__instance.state).toBe('payeeResolved'); @@ -384,8 +373,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state let result = await resultPromise; - console.log(`Result after resolve payee: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE'); expect(StateMachine.__instance.state).toBe('payeeResolved'); @@ -413,8 +400,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state result = await resultPromise; - console.log(`Result after request quote: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE'); expect(StateMachine.__instance.state).toBe('quoteReceived'); @@ -444,8 +429,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state let result = await resultPromise; - console.log(`Result after resolve payee: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('WAITING_FOR_PARTY_ACCEPTANCE'); expect(StateMachine.__instance.state).toBe('payeeResolved'); @@ -473,8 +456,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state result = await resultPromise; - console.log(`Result after request quote: ${util.inspect(result)}`); - // check we stopped at quoteReceived state expect(result.currentState).toBe('WAITING_FOR_QUOTE_ACCEPTANCE'); expect(StateMachine.__instance.state).toBe('quoteReceived'); @@ -500,8 +481,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state result = await resultPromise; - console.log(`Result after transfer fulfil: ${util.inspect(result)}`); - // check we stopped at quoteReceived state expect(result.currentState).toBe('COMPLETED'); expect(StateMachine.__instance.state).toBe('succeeded'); @@ -553,8 +532,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state const result = await resultPromise; - console.log(`Result after three stage transfer: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('COMPLETED'); expect(StateMachine.__instance.state).toBe('succeeded'); @@ -606,8 +583,6 @@ describe('outboundModel', () => { // wait for the model to reach a terminal state const result = await resultPromise; - console.log(`Result after three stage transfer: ${util.inspect(result)}`); - // check we stopped at payeeResolved state expect(result.currentState).toBe('COMPLETED'); expect(StateMachine.__instance.state).toBe('succeeded'); @@ -836,7 +811,7 @@ describe('outboundModel', () => { async function testTlsServer(enableTls) { - config.tls.outbound.mutualTLS.enabled = enableTls; + config.outbound.tls.mutualTLS.enabled = enableTls; new Model({ cache, diff --git a/src/test/unit/lib/model/PartiesModel.test.js b/src/test/unit/lib/model/PartiesModel.test.js new file mode 100644 index 000000000..fe98c73ca --- /dev/null +++ b/src/test/unit/lib/model/PartiesModel.test.js @@ -0,0 +1,306 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2019 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; + +// we use a mock standard components lib to intercept and mock certain funcs +jest.mock('@mojaloop/sdk-standard-components'); + +const { uuid } = require('uuidv4'); +const Model = require('@internal/model').PartiesModel; +const { MojaloopRequests } = require('@mojaloop/sdk-standard-components'); +const defaultConfig = require('./data/defaultConfig'); +const mockLogger = require('../../mockLogger'); + +describe('PartiesModel', () => { + let cacheKey; + let data; + let modelConfig; + + const subId = 123; + let handler = null; + beforeEach(async () => { + + modelConfig = { + logger: mockLogger({app: 'PartiesModel-test'}), + + // there is no need to mock redis but only Cache + cache: { + get: jest.fn(() => Promise.resolve(data)), + set: jest.fn(() => Promise.resolve), + + // mock subscription and store handler + subscribe: jest.fn(async (channel, h) => { + handler = jest.fn(h); + return subId; + }), + + // mock publish and call stored handler + publish: jest.fn(async (channel, message) => await handler(channel, message, subId)), + + unsubscribe: jest.fn(() => Promise.resolve()) + }, + ...defaultConfig + }; + data = { the: 'mocked data' }; + + cacheKey = 'cache-key'; + }); + + describe('create', () => { + test('proper creation of model', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + + expect(model.state).toBe('start'); + + // model's methods layout + const methods = [ + 'run', 'getResponse', + 'onRequestPartiesInformation' + ]; + + methods.forEach((method) => expect(typeof model[method]).toEqual('function')); + }); + }); + + describe('getResponse', () => { + + it('should remap currentState', async () => { + const model = await Model.create(data, cacheKey, modelConfig); + const states = model.allStates(); + // should remap for all states except 'init' and 'none' + states.filter((s) => s !== 'init' && s !== 'none').forEach((state) => { + model.context.data.currentState = state; + const result = model.getResponse(); + expect(result.currentState).toEqual(Model.mapCurrentState[state]); + }); + + }); + + it('should handle unexpected state', async() => { + const model = await Model.create(data, cacheKey, modelConfig); + + // simulate lack of state by undefined property + delete model.context.data.currentState; + + const resp = model.getResponse(); + expect(resp.currentState).toEqual(Model.mapCurrentState.errored); + + // ensure that we log the problem properly + expect(modelConfig.logger.error).toHaveBeenCalledWith(`Parties model response being returned from an unexpected state: ${undefined}. Returning ERROR_OCCURRED state`); + }); + }); + + describe('channelName', () => { + it('should validate input', () => { + expect(Model.channelName()).toEqual('parties-undefined-undefined-undefined'); + }); + + it('should generate proper channel name', () => { + const type = uuid(); + const id = uuid(); + expect(Model.channelName(type, id)).toEqual(`parties-${type}-${id}-undefined`); + }); + }); + + describe('onRequestPartiesInformation', () => { + + it('should implement happy flow', async () => { + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const channel = Model.channelName(type, id, subIdValue); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + // mock workflow execution which is tested in separate case + model.run = jest.fn(() => Promise.resolve()); + + const message = { + party: { + Iam: 'the-body' + } + }; + + // manually invoke transition handler + model.onRequestPartiesInformation(model.fsm, type, id, subIdValue) + .then(() => { + // subscribe should be called only once + expect(cache.subscribe).toBeCalledTimes(1); + + // subscribe should be done to proper notificationChannel + expect(cache.subscribe.mock.calls[0][0]).toEqual(channel); + + // check invocation of request.getParties + expect(MojaloopRequests.__getParties).toBeCalledWith(type, id, subIdValue); + + // check that this.context.data is updated + expect(model.context.data).toEqual({ + ...message, + // current state will be updated by onAfterTransition which isn't called + // when manual invocation of transition handler happens + currentState: 'start' + }); + }); + + // ensure handler wasn't called before publishing the message + expect(handler).not.toBeCalled(); + + // ensure that cache.unsubscribe does not happened before fire the message + expect(cache.unsubscribe).not.toBeCalled(); + + + // fire publication with given message + await cache.publish(channel, JSON.stringify(message)); + + // handler should be called only once + expect(handler).toBeCalledTimes(1); + + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(channel, subId); + }); + + it('should unsubscribe from cache in case when error happens in workflow run', async () => { + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const channel = Model.channelName(type, id, subIdValue); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + // invoke transition handler + model.onRequestPartiesInformation(model.fsm, type, id, subIdValue).catch((err) => { + expect(err.message).toEqual('Unexpected token u in JSON at position 0'); + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(channel, subId); + }); + + // fire publication to channel with invalid message + // should throw the exception from JSON.parse + await cache.publish(channel, undefined); + + }); + + it('should unsubscribe from cache in case when error happens Mojaloop requests', async () => { + // simulate error + MojaloopRequests.__getParties = jest.fn(() => Promise.reject('getParties failed')); + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const channel = Model.channelName(type, id, subIdValue); + const model = await Model.create(data, cacheKey, modelConfig); + const { cache } = model.context; + + let theError = null; + // invoke transition handler + try { + await model.onRequestPartiesInformation(model.fsm, type, id, subIdValue); + throw new Error('this point should not be reached'); + } catch (error) { + theError = error; + } + expect(theError).toEqual('getParties failed'); + // handler should unsubscribe from notification channel + expect(cache.unsubscribe).toBeCalledTimes(1); + expect(cache.unsubscribe).toBeCalledWith(channel, subId); + }); + + }); + + describe('run workflow', () => { + it('start', async () => { + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const model = await Model.create(data, cacheKey, modelConfig); + + model.requestPartiesInformation = jest.fn(); + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'start'; + const result = await model.run(type, id, subIdValue); + expect(result).toEqual({the: 'response'}); + expect(model.requestPartiesInformation).toBeCalledTimes(1); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log.mock.calls).toEqual([ + ['State machine transitioned \'init\': none -> start'], + [`Party information requested for /${type}/${id}/${subIdValue}, currentState: start`], + ['Party information retrieved successfully'], + [`Persisted model in cache: ${cacheKey}`], + ]); + }); + it('succeeded', async () => { + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'succeeded'; + const result = await model.run(type, id, subIdValue); + + expect(result).toEqual({the: 'response'}); + expect(model.getResponse).toBeCalledTimes(1); + expect(model.context.logger.log).toBeCalledWith('Party information retrieved successfully'); + }); + + it('errored', async () => { + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const model = await Model.create(data, cacheKey, modelConfig); + + model.getResponse = jest.fn(() => Promise.resolve({the: 'response'})); + + model.context.data.currentState = 'errored'; + const result = await model.run(type, id, subIdValue); + + expect(result).toBeFalsy(); + expect(model.getResponse).not.toBeCalled(); + expect(model.context.logger.log).toBeCalledWith('State machine in errored state'); + }); + + it('should handle errors', async () => { + const type = uuid(); + const id = uuid(); + const subIdValue = uuid(); + + const model = await Model.create(data, cacheKey, modelConfig); + + model.requestPartiesInformation = jest.fn(() => { + const err = new Error('requestPartiesInformation failed'); + err.requestPartiesInformationState = 'some'; + return Promise.reject(err); + }); + model.error = jest.fn(); + model.context.data.currentState = 'start'; + + let theError = null; + try { + await model.run(type, id, subIdValue); + throw new Error('this point should not be reached'); + } catch(error) { + theError = error; + } + // check propagation of original error + expect(theError.message).toEqual('requestPartiesInformation failed'); + + // ensure we start transition to errored state + expect(model.error).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/test/unit/lib/model/common/PersistentStateMachine.test.js b/src/test/unit/lib/model/common/PersistentStateMachine.test.js new file mode 100644 index 000000000..bf618dc96 --- /dev/null +++ b/src/test/unit/lib/model/common/PersistentStateMachine.test.js @@ -0,0 +1,176 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +'use strict'; + +const Cache = jest.createMockFromModule('@internal/cache'); + +const PSM = require('@internal/model').PersistentStateMachine; +const mockLogger = require('../../../mockLogger'); +describe('PersistentStateMachine', () => { + let cache; + let data; + let smSpec; + const key = 'cache-key'; + + const logger = mockLogger({app: 'persistent-state-machine-test'}); + + function checkPSMLayout(psm, optData) { + expect(psm).toBeTruthy(); + + expect(psm.state).toEqual((optData && optData.currentState) || smSpec.init || 'none'); + + expect(psm.context).toEqual({ + // allow passing optional data, elsewhere use default + data: optData || data, + cache, + key, + logger + }); + + expect(typeof psm.onAfterTransition).toEqual('function'); + expect(typeof psm.onPendingTransition).toEqual('function'); + expect(typeof psm.saveToCache).toEqual('function'); + expect(typeof psm.init).toEqual('function'); + expect(typeof psm.gogo).toEqual('function'); + expect(typeof psm.error).toEqual('function'); + } + + function shouldNotBeExecuted() { + throw new Error('test failure enforced: this code should never be executed'); + } + + beforeEach(async () => { + smSpec = { + // init: 'start', + transitions: [ + { name: 'init', from: 'none', to: 'start'}, + { name: 'gogo', from: 'start', to: 'end' }, + { name: 'error', from: '*', to: 'errored' } + ], + methods: { + onGogo: async () => { + return new Promise( (resolved) => { + setTimeout((() => resolved(true)), 100); + } ); + }, + onError: () => { + logger.log('onError'); + } + } + }; + + // test data + data = { the: 'data' }; + + cache = new Cache({ + host: 'dummycachehost', + port: 1234, + logger, + }); + // mock cache set & get + cache.get = jest.fn(async () => data); + cache.set = jest.fn(async () => 'cache set replies'); + + await cache.connect(); + }); + + afterEach(async () => { + await cache.disconnect(); + }); + + test('module layout', () => { + expect(typeof PSM.create).toEqual('function'); + expect(typeof PSM.loadFromCache).toEqual('function'); + }); + + test('create', async () => { + const psm = await PSM.create(data, cache, key, logger, smSpec); + checkPSMLayout(psm); + expect(psm.state).toEqual('none'); + await psm.init(); + expect(psm.state).toEqual('start'); + }); + + describe('onPendingTransition', () => { + it('should throw error if not `error` transition', async () => { + const psm = await PSM.create(data, cache, key, logger, smSpec); + checkPSMLayout(psm); + + psm.init(); + expect(() => psm.gogo()).toThrowError('Transition \'gogo\' requested while another transition is in progress'); + + }); + + it('should not throw error if `error` transition called when `gogo` is pending', (done) => { + PSM.create(data, cache, key, logger, smSpec).then((psm) => { + checkPSMLayout(psm); + + psm.init() + .then(() => { + expect(psm.state).toEqual('start'); + psm.gogo(); + expect(psm.state).toEqual('end'); + return Promise.resolve(); + }) + .then(() => psm.error()) + .then(done) + .catch(shouldNotBeExecuted); + }); + }); + }); + + describe('loadFromCache', () => { + it('should properly call cache.get, get expected data in `context.data` and setup state of machine', async () => { + const dataFromCache = { this_is: 'data from cache', currentState: 'end'}; + cache.get = jest.fn( async () => dataFromCache); + const psm = await PSM.loadFromCache(cache, key, logger, smSpec); + checkPSMLayout(psm, dataFromCache); + + // to get value from cache proper key should be used + expect(cache.get).toHaveBeenCalledWith(key); + + // check what has been stored in `context.data` + expect(psm.context.data).toEqual(dataFromCache); + + }); + + it('should throw when received invalid data from `cache.get`', async () => { + cache.get = jest.fn( async () => null); + try { + await PSM.loadFromCache(cache, key, logger, smSpec); + shouldNotBeExecuted(); + } catch (error) { + expect(error.message).toEqual(`No cached data found for: ${key}`); + } + }); + + it('should propagate error received from `cache.get`', async () => { + cache.get = jest.fn( async () => { throw new Error('error from cache.get'); }); + expect(() => PSM.loadFromCache(cache, key, logger, smSpec)) + .rejects.toEqual(new Error('error from cache.get')); + }); + }); + + describe('saveToCache', () => { + + it('should rethrow error from cache.set', async () => { + + // mock to simulate throwing error + cache.set = jest.fn(() => { throw new Error('error from cache.set'); }); + + const psm = await PSM.create(data, cache, key, logger, smSpec); + checkPSMLayout(psm); + + expect(() => psm.saveToCache()).rejects.toEqual(new Error('error from cache.set')); + }); + }); + +}); diff --git a/src/test/unit/lib/model/data/bulkQuoteRequest.json b/src/test/unit/lib/model/data/bulkQuoteRequest.json new file mode 100644 index 000000000..8d34c0eec --- /dev/null +++ b/src/test/unit/lib/model/data/bulkQuoteRequest.json @@ -0,0 +1,27 @@ +{ + "from": { + "displayName": "Steven Oderayi", + "idType": "MSISDN", + "idValue": "234567890" + }, + "individualQuotes": [ + { + "to": { + "fspId": "fake-fsp-id", + "idType": "PERSONAL_ID", + "idValue": "123456789", + "idSubValue": "PASSPORT" + }, + "amountType": "SEND", + "currency": "USD", + "amount": "100", + "transactionType": "TRANSFER", + "note": "test payment" + } + ], + "homeTransactionId": "123ABC", + "extensions": [ + { "key": "qkey1", "value": "qvalue1" }, + { "key": "qkey2", "value": "qvalue2" } + ] +} diff --git a/src/test/unit/lib/model/data/bulkQuoteResponse.json b/src/test/unit/lib/model/data/bulkQuoteResponse.json new file mode 100644 index 000000000..72cc09abf --- /dev/null +++ b/src/test/unit/lib/model/data/bulkQuoteResponse.json @@ -0,0 +1,35 @@ +{ + "type": "bulkQuoteResponse", + "data": { + "individualQuoteResults": [ + { + "transferAmount": { + "amount": "500", + "currency": "USD" + }, + "payeeReceiveAmount": { + "amount": "490", + "currency": "USD" + }, + "payeeFspFee": { + "amount": "5", + "currency": "USD" + }, + "payeeFspCommission": { + "amount": "5", + "currency": "USD" + } + } + ], + "geoCode": { + "latitude": "53.295971", + "longitude": "-0.038500" + }, + "expiration": "2017-11-15T14:17:09.663+01:00", + "ilpPacket": "AQAAAAAAACasIWcuc2UubW9iaWxlbW9uZXkubXNpc2RuLjEyMzQ1Njc4OYIEIXsNCiAgICAidHJhbnNhY3Rpb25JZCI6ICI4NWZlYWMyZi0zOWIyLTQ5MWItODE3ZS00YTAzMjAzZDRmMTQiLA0KICAgICJxdW90ZUlkIjogIjdjMjNlODBjLWQwNzgtNDA3Ny04MjYzLTJjMDQ3ODc2ZmNmNiIsDQogICAgInBheWVlIjogew0KICAgICAgICAicGFydHlJZEluZm8iOiB7DQogICAgICAgICAgICAicGFydHlJZFR5cGUiOiAiTVNJU0ROIiwNCiAgICAgICAgICAgICJwYXJ0eUlkZW50aWZpZXIiOiAiMTIzNDU2Nzg5IiwNCiAgICAgICAgICAgICJmc3BJZCI6ICJNb2JpbGVNb25leSINCiAgICAgICAgfSwNCiAgICAgICAgInBlcnNvbmFsSW5mbyI6IHsNCiAgICAgICAgICAgICJjb21wbGV4TmFtZSI6IHsNCiAgICAgICAgICAgICAgICAiZmlyc3ROYW1lIjogIkhlbnJpayIsDQogICAgICAgICAgICAgICAgImxhc3ROYW1lIjogIkthcmxzc29uIg0KICAgICAgICAgICAgfQ0KICAgICAgICB9DQogICAgfSwNCiAgICAicGF5ZXIiOiB7DQogICAgICAgICJwZXJzb25hbEluZm8iOiB7DQogICAgICAgICAgICAiY29tcGxleE5hbWUiOiB7DQogICAgICAgICAgICAgICAgImZpcnN0TmFtZSI6ICJNYXRzIiwNCiAgICAgICAgICAgICAgICAibGFzdE5hbWUiOiAiSGFnbWFuIg0KICAgICAgICAgICAgfQ0KICAgICAgICB9LA0KICAgICAgICAicGFydHlJZEluZm8iOiB7DQogICAgICAgICAgICAicGFydHlJZFR5cGUiOiAiSUJBTiIsDQogICAgICAgICAgICAicGFydHlJZGVudGlmaWVyIjogIlNFNDU1MDAwMDAwMDA1ODM5ODI1NzQ2NiIsDQogICAgICAgICAgICAiZnNwSWQiOiAiQmFua05yT25lIg0KICAgICAgICB9DQogICAgfSwNCiAgICAiYW1vdW50Ijogew0KICAgICAgICAiYW1vdW50IjogIjEwMCIsDQogICAgICAgICJjdXJyZW5jeSI6ICJVU0QiDQogICAgfSwNCiAgICAidHJhbnNhY3Rpb25UeXBlIjogew0KICAgICAgICAic2NlbmFyaW8iOiAiVFJBTlNGRVIiLA0KICAgICAgICAiaW5pdGlhdG9yIjogIlBBWUVSIiwNCiAgICAgICAgImluaXRpYXRvclR5cGUiOiAiQ09OU1VNRVIiDQogICAgfSwNCiAgICAibm90ZSI6ICJGcm9tIE1hdHMiDQp9DQo\u003d\u003d", + "condition": "fH9pAYDQbmoZLPbvv3CSW2RfjU4jvM4ApG_fqGnR7Xs" + }, + "headers": { + "fspiop-source": "foo" + } +} diff --git a/src/test/unit/lib/model/data/bulkTransferFulfil.json b/src/test/unit/lib/model/data/bulkTransferFulfil.json new file mode 100644 index 000000000..52f0a177f --- /dev/null +++ b/src/test/unit/lib/model/data/bulkTransferFulfil.json @@ -0,0 +1,13 @@ +{ + "type": "bulkTransferFulfil", + "data": { + "individualTransferResults": [ + { + "transferId": "fake-transfer-id", + "fulfilment": "87mm1-reS3SAi8oIWXgBkLmgWc1MkZ_yLbFDX5XAdo5o" + } + ], + "completedTimestamp": "2017-11-15T14:16:09.663+01:00", + "bulkTransferState": "COMMITTED" + } +} diff --git a/src/test/unit/lib/model/data/bulkTransferRequest.json b/src/test/unit/lib/model/data/bulkTransferRequest.json new file mode 100644 index 000000000..fd4f3c8e6 --- /dev/null +++ b/src/test/unit/lib/model/data/bulkTransferRequest.json @@ -0,0 +1,29 @@ +{ + "from": { + "displayName": "James Bush", + "idType": "MSISDN", + "idValue": "447710066017" + }, + "payeeFsp": { + "fspId": "fake-payeefsp-id" + }, + "homeTransactionId": "123ABC", + "individualTransfers": [ + { + "to": { + "idType": "PERSONAL_ID", + "idValue": "123456789", + "idSubValue": "PASSPORT" + }, + "amountType": "SEND", + "currency": "USD", + "amount": "100", + "transactionType": "TRANSFER", + "note": "test payment" + } + ], + "extensions": [ + { "key": "tkey1", "value": "tvalue1" }, + { "key": "tkey2", "value": "tvalue2" } + ] +} diff --git a/src/test/unit/lib/model/data/defaultConfig.json b/src/test/unit/lib/model/data/defaultConfig.json index ec54225e2..aae68ea22 100644 --- a/src/test/unit/lib/model/data/defaultConfig.json +++ b/src/test/unit/lib/model/data/defaultConfig.json @@ -1,6 +1,26 @@ { - "inboundPort": 4000, - "outboundPort": 4001, + "inbound": { + "port": 4000, + "tls": { + "mutualTLS": { "enabled": false }, + "creds": { + "ca": null, + "cert": null, + "key": null + } + } + }, + "outbound": { + "port": 4001, + "tls": { + "mutualTLS": { "enabled": false }, + "creds": { + "ca": null, + "cert": null, + "key": null + } + } + }, "peerEndpoint": "172.17.0.2:3001", "backendEndpoint": "172.17.0.2:3001", "alsEndpoint": "127.0.0.1:6500", @@ -12,24 +32,6 @@ "autoAcceptQuotes": true, "autoAcceptParty": true, "useQuoteSourceFSPAsTransferPayeeFSP": false, - "tls": { - "inbound": { - "mutualTLS": { "enabled": false }, - "creds": { - "ca": null, - "cert": null, - "key": null - } - }, - "outbound": { - "mutualTLS": { "enabled": false }, - "creds": { - "ca": null, - "cert": null, - "key": null - } - } - }, "validateInboundJws": true, "validateInboundPutPartiesJws": false, "jwsSign": true, @@ -45,8 +47,10 @@ "enabled": false, "listenPort": 6000 }, - "wso2Auth": { - "refreshSeconds": 3600 + "wso2": { + "auth": { + "refreshSeconds": 3600 + } }, "rejectExpiredQuoteResponses": false, "rejectExpiredTransferFulfils": false, diff --git a/src/test/unit/lib/model/data/getBulkTransfersBackendResponse.json b/src/test/unit/lib/model/data/getBulkTransfersBackendResponse.json new file mode 100644 index 000000000..d644eabe0 --- /dev/null +++ b/src/test/unit/lib/model/data/getBulkTransfersBackendResponse.json @@ -0,0 +1,42 @@ +{ + "homeTransactionId": "home-tx-001", + "from": { + "displayName": "John Doe", + "idType": "MSISDN", + "idValue": "123456789" + }, + "internalRequest": { + "individualTransfers": [ + { + "transferId": "fake-transfer-id", + "to": { + "idType": "PERSONAL_ID", + "idValue": "987654321", + "idSubValue": "PASSPORT", + "fspId": "mojaloop-sdk" + }, + "amount": "100", + "currency": "USD", + "amountType": "SEND", + "transactionType": { + "initiator": "PAYER", + "initiatorType": "CONSUMER", + "scenario": "TRANSFER" + } + } + ] + }, + "bulkTransferState": "COMMITTED", + "timestamp": "2019-11-15T14:16:09.663", + "note": "test payment", + "extensions": [ + { + "key": "treskey1", + "value": "tresvalue1" + }, + { + "key": "treskey2", + "value": "tresvalue2" + } + ] +} \ No newline at end of file diff --git a/src/test/unit/lib/model/data/getBulkTransfersMojaloopResponse.json b/src/test/unit/lib/model/data/getBulkTransfersMojaloopResponse.json new file mode 100644 index 000000000..e7e58fa98 --- /dev/null +++ b/src/test/unit/lib/model/data/getBulkTransfersMojaloopResponse.json @@ -0,0 +1,22 @@ +{ + "completedTimestamp": "2019-11-15T14:16:09.663", + "extensionList": { + "extension": [ + { + "key": "treskey1", + "value": "tresvalue1" + }, + { + "key": "treskey2", + "value": "tresvalue2" + } + ] + }, + "bulkTransferState": "COMMITTED", + "individualTransferResults": [ + { + "transferId": "fake-transfer-id", + "fulfilment": "mockGeneratedFulfilment" + } + ] +} diff --git a/src/test/unit/lib/model/data/mockArguments.json b/src/test/unit/lib/model/data/mockArguments.json index b6650ac9a..b010d9d07 100644 --- a/src/test/unit/lib/model/data/mockArguments.json +++ b/src/test/unit/lib/model/data/mockArguments.json @@ -60,5 +60,72 @@ "internalGetOTPResponse" : { "value": "123456" }, - "fspId": "fake-fsp-id" + "fspId": "fake-fsp-id", + "bulkQuoteRequest": { + "bulkQuoteId": "fake-bulk-quote-id", + "payer": { + "partyIdInfo": { + "partyIdType": "MSISDN", + "partyIdentifier": "17855501914", + "fspId": "mojaloop-sdk" + }, + "personalInfo": { + "complexName": { + "firstName": "John", + "lastName": "Doe" + }, + "dateOfBirth": "2010-10-10" + }, + "name": "John Doe", + "merchantClassificationCode": "123" + }, + "expiration": "2019-06-04T04:02:10.378Z", + "individualQuotes": [ + { + "quoteId": "fake-quote-id", + "transactionId": "fake-transaction-id", + "payee": { + "partyIdInfo": { + "partyIdType": "PERSONAL_ID", + "partyIdentifier": "123456789", + "partySubIdOrType": "PASSPORT", + "fspId": "goldenpayeefsp" + }, + "personalInfo": { + "complexName": { + "firstName": "Jane", + "lastName": "Doe" + }, + "dateOfBirth": "2010-10-10" + }, + "name": "Jane Doe", + "merchantClassificationCode": "456" + }, + "amountType": "SEND", + "amount": { + "currency": "XOF", + "amount": 10 + }, + "transactionType": { + "scenario": "TRANSFER", + "initiator": "PAYER", + "initiatorType": "CONSUMER" + } + } + ] + }, + "internalBulkQuoteResponse": { + "expiration": "2019-11-12T09:02:10.378Z", + "individualQuoteResults": [ + { + "quoteId": "fake-quote-id", + "transferAmount": 500, + "transferAmountCurrency": "XOF", + "payeeReceiveAmount": 490, + "payeeFspFee": 10, + "payeeFspCommission": 0, + "condition": "fH9pAYDQbmoZLPbvv3CSW2RfjU4jvM4ApG_fqGnR7Xs" + } + ] + } } diff --git a/src/test/unit/lib/model/data/mockAuthorizationArguments.json b/src/test/unit/lib/model/data/mockAuthorizationArguments.json new file mode 100644 index 000000000..0f618b5c8 --- /dev/null +++ b/src/test/unit/lib/model/data/mockAuthorizationArguments.json @@ -0,0 +1,60 @@ +{ + "authorizationRequest": { + "authenticationType": "U2F", + "retriesLeft": 1, + "amount": { + "currency": "USD", + "amount": "100" + }, + "transactionId": "2f169631-ef99-4cb1-96dc-91e8fc08f539", + "transactionRequestId": "02e28448-3c05-4059-b5f7-d518d0a2d8ea", + "quote": { + "transferAmount": { + "currency": "USD", + "amount": "100" + }, + "payeeReceiveAmount": { + "currency": "USD", + "amount": "99" + }, + "payeeFspFee": { + "currency": "USD", + "amount": "1" + }, + "payeeFspCommission": { + "currency": "USD", + "amount": "0" + }, + "expiration": "2020-05-17T15:28:54.250Z", + "geoCode": { + "latitude": "+45.4215", + "longitude": "+75.6972" + }, + "ilpPacket": "AYIBgQAAAAAAAASwNGxldmVsb25lLmRmc3AxLm1lci45T2RTOF81MDdqUUZERmZlakgy...", + "condition": "f5sqb7tBTWPd5Y8BDFdMm9BJR_MNI4isf8p8n4D5pHA", + "extensionList": { + "extension": [ + { + "key": "errorDescription1", + "value": "This is a more detailed error description" + } + ] + } + } + }, + "internalSignedChallengeResponse": { + "pinValue": "123456", + "counter": "1" + }, + "authorizationsResponse": { + "authenticationInfo": { + "authentication": "U2F", + "authenticationValue": { + "pinValue": "123456", + "counter": "1" + } + }, + "responseType": "ENTERED" + }, + "fspId": "fake-fsp-id" +} \ No newline at end of file diff --git a/src/test/unit/lib/model/data/notificationToPayee.json b/src/test/unit/lib/model/data/notificationToPayee.json new file mode 100644 index 000000000..49c2a1ab2 --- /dev/null +++ b/src/test/unit/lib/model/data/notificationToPayee.json @@ -0,0 +1,10 @@ +{ + "type": "notificationToPayee", + "headers": { + "fspiop-source": "SWITCH" + }, + "data": { + "completedTimestamp": "2017-11-15T14:16:09.663+01:00", + "transferState": "COMMITTED" + } +} diff --git a/src/test/unit/mockLogger.js b/src/test/unit/mockLogger.js new file mode 100644 index 000000000..32ba36363 --- /dev/null +++ b/src/test/unit/mockLogger.js @@ -0,0 +1,30 @@ +/************************************************************************** + * (C) Copyright ModusBox Inc. 2020 - All rights reserved. * + * * + * This file is made available under the terms of the license agreement * + * specified in the corresponding source code repository. * + * * + * ORIGINAL AUTHOR: * + * Paweł Marzec - pawel.marzec@modusbox.com * + **************************************************************************/ + +const { Logger } = require('@mojaloop/sdk-standard-components'); + +function mockLogger(context, keepQuiet) { + // if keepQuite is undefined then be quiet + if(keepQuiet || typeof keepQuiet === 'undefined') { + const log = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn() + }; + return { + ...log, + push: jest.fn(() => log) + }; + } + // let be elaborative and dir logging to console + return new Logger({ context: context, space: 4 }); +} + +module.exports = mockLogger; diff --git a/src/test/unit/outboundApi/data/bulkQuoteRequest.json b/src/test/unit/outboundApi/data/bulkQuoteRequest.json new file mode 100644 index 000000000..87037f899 --- /dev/null +++ b/src/test/unit/outboundApi/data/bulkQuoteRequest.json @@ -0,0 +1,28 @@ +{ + "homeTransactionId": "123ABC", + "from": { + "displayName": "James Bush", + "idType": "MSISDN", + "idValue": "447710066017", + "extensionList": [ + { + "key": "accountType", + "value": "Wallet" + } + ] + }, + "individualQuotes": [ + { + "quoteId": "12346789", + "to": { + "idType": "MSISDN", + "idValue": "1234567890" + }, + "amountType": "SEND", + "currency": "USD", + "amount": "100", + "transactionType": "TRANSFER", + "note": "test payment" + } + ] +} \ No newline at end of file diff --git a/src/test/unit/outboundApi/data/bulkTransferRequest.json b/src/test/unit/outboundApi/data/bulkTransferRequest.json new file mode 100644 index 000000000..881ee3857 --- /dev/null +++ b/src/test/unit/outboundApi/data/bulkTransferRequest.json @@ -0,0 +1,28 @@ +{ + "homeTransactionId": "123ABC", + "from": { + "displayName": "James Bush", + "idType": "MSISDN", + "idValue": "447710066017", + "extensionList": [ + { + "key": "accountType", + "value": "Wallet" + } + ] + }, + "individualTransfers": [ + { + "transferId": "132435", + "to": { + "idType": "MSISDN", + "idValue": "1234567890" + }, + "amountType": "SEND", + "currency": "USD", + "amount": "100", + "transactionType": "TRANSFER", + "note": "test payment" + } + ] +} \ No newline at end of file diff --git a/src/test/unit/outboundApi/data/mockBulkQuoteError.json b/src/test/unit/outboundApi/data/mockBulkQuoteError.json new file mode 100644 index 000000000..ca6eb7906 --- /dev/null +++ b/src/test/unit/outboundApi/data/mockBulkQuoteError.json @@ -0,0 +1,45 @@ +{ + "message": "Mock error", + "httpStatusCode": 500, + "bulkQuoteState": { + "from": { + "displayName": "James Bush", + "idType": "MSISDN", + "idValue": "447710066017" + }, + "individualQuoteResults": [ + { + "to": { + "idType": "MSISDN", + "idValue": "1234567890" + }, + "amountType": "SEND", + "currency": "USD", + "amount": "100", + "transactionType": "TRANSFER", + "note": "test payment", + "homeTransactionId": "123ABC", + "transferId": "5a2ad5dc-4ab1-4a22-8c5b-62f75252a8d5", + "currentState": "ERROR_OCCURRED" + } + ], + "lastError": { + "httpStatusCode": 500, + "mojaloopError": { + "errorInformation": { + "errorCode": "3204", + "errorDescription": "Party not found", + "extensionList": { + "extension": [{ + "key": "someKey", + "value": "someValue" + }, { + "key": "extErrorKey", + "value": "9999" + }] + } + } + } + } + } +} diff --git a/src/test/unit/outboundApi/data/mockBulkTransferError.json b/src/test/unit/outboundApi/data/mockBulkTransferError.json new file mode 100644 index 000000000..00f6772b9 --- /dev/null +++ b/src/test/unit/outboundApi/data/mockBulkTransferError.json @@ -0,0 +1,48 @@ +{ + "message": "Mock error", + "statusCode": 500, + "bulkTransferState": { + "from": { + "displayName": "James Bush", + "idType": "MSISDN", + "idValue": "447710066017" + }, + "to": { + "idType": "MSISDN", + "idValue": "1234567890" + }, + "individualTransferResults": [ + { + "transferId": "5a2ad5dc-4ab1-4a22-8c5b-62f75252a8d5", + "amountType": "SEND", + "currency": "USD", + "amount": "100", + "transactionType": "TRANSFER", + "note": "test payment", + "homeTransactionId": "123ABC", + "currentState": "ERROR_OCCURRED" + } + ], + "lastError": { + "httpStatusCode": 500, + "mojaloopError": { + "errorInformation": { + "errorCode": "3204", + "errorDescription": "Party not found", + "extensionList": { + "extension": [ + { + "key": "someKey", + "value": "someValue" + }, + { + "key": "extErrorKey", + "value": "9999" + } + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/src/test/unit/outboundApi/handlers.test.js b/src/test/unit/outboundApi/handlers.test.js index 48a7f9fed..8e3bed363 100644 --- a/src/test/unit/outboundApi/handlers.test.js +++ b/src/test/unit/outboundApi/handlers.test.js @@ -11,16 +11,29 @@ 'use strict'; const mockError = require('./data/mockError'); +const mockBulkQuoteError = require('./data/mockBulkQuoteError'); +const mockBulkTransferError = require('./data/mockBulkTransferError'); const mockRequestToPayError = require('./data/mockRequestToPayError'); const mockRequestToPayTransferError = require('./data/mockRequestToPayTransferError'); const transferRequest = require('./data/transferRequest'); +const bulkTransferRequest = require('./data/bulkTransferRequest'); +const bulkQuoteRequest = require('./data/bulkQuoteRequest'); const requestToPayPayload = require('./data/requestToPay'); const requestToPayTransferRequest = require('./data/requestToPayTransferRequest'); +const mockLogger = require('../mockLogger'); jest.mock('@internal/model'); const handlers = require('../../../OutboundServer/handlers'); -const { OutboundTransfersModel, OutboundRequestToPayTransferModel, OutboundRequestToPayModel } = require('@internal/model'); +const { + OutboundTransfersModel, + OutboundBulkTransfersModel, + OutboundBulkQuotesModel, + OutboundRequestToPayTransferModel, + OutboundRequestToPayModel, + OutboundAuthorizationsModel, + PartiesModel, +} = require('@internal/model'); /** * Mock the outbound transfer model to simulate throwing errors @@ -42,6 +55,40 @@ OutboundTransfersModel.mockImplementation(() => { }; }); +/** + * Mock the outbound bulk transfers model to simulate throwing errors + */ +OutboundBulkTransfersModel.mockImplementation(() => { + return { + run: async () => { + throw mockBulkTransferError; + }, + initialize: async () => { + return; + }, + load: async () => { + return; + } + }; +}); + +/** + * Mock the outbound bulk quotes model to simulate throwing errors + */ +OutboundBulkQuotesModel.mockImplementation(() => { + return { + run: async () => { + throw mockBulkQuoteError; + }, + initialize: async () => { + return; + }, + load: async () => { + return; + } + }; +}); + /** * Mock the outbound transfer model to simulate throwing errors */ @@ -96,12 +143,12 @@ describe('Outbound API handlers:', () => { response: {}, state: { conf: {}, - logger: console + logger: mockLogger({ app: 'outbound-api-handlers-test'}) } }; await handlers['/transfers'].post(mockContext); - + // check response is correct expect(mockContext.response.status).toEqual(500); expect(mockContext.response.body).toBeTruthy(); @@ -124,7 +171,7 @@ describe('Outbound API handlers:', () => { conf: { outboundErrorStatusCodeExtensionKey: 'extErrorKey' // <- tell the handler to use this extensionList item as source of statusCode }, - logger: console + logger: mockLogger({ app: 'outbound-api-handlers-test'}) } }; @@ -142,11 +189,13 @@ describe('Outbound API handlers:', () => { }); }); - describe('POST /requestToPayTransfer', () => { + describe('PUT /transfers', () => { test('returns correct error response body when model throws mojaloop error', async () => { const mockContext = { request: { - body: requestToPayTransferRequest, + body: { + acceptQuote: true + }, headers: { 'fspiop-source': 'foo' } @@ -154,29 +203,89 @@ describe('Outbound API handlers:', () => { response: {}, state: { conf: {}, - logger: console + logger: mockLogger({ app: 'outbound-api-handlers-test'}), + path: { + params: { + transferId: '12345' + } + } } }; - await handlers['/requestToPayTransfer'].post(mockContext); + await handlers['/transfers/{transferId}'].put(mockContext); + // check response is correct expect(mockContext.response.status).toEqual(500); expect(mockContext.response.body).toBeTruthy(); expect(mockContext.response.body.message).toEqual('Mock error'); - expect(mockContext.response.body.statusCode) - .toEqual(mockRequestToPayTransferError.requestToPayTransferState.lastError.mojaloopError.errorInformation.errorCode); - expect(mockContext.response.body.requestToPayTransferState).toEqual(mockRequestToPayTransferError.requestToPayTransferState); + expect(mockContext.response.body.statusCode).toEqual('3204'); + expect(mockContext.response.body.transferState).toEqual(mockError.transferState); }); }); - - describe('PUT /transfers', () => { + describe('POST /bulkTransfers', () => { test('returns correct error response body when model throws mojaloop error', async () => { const mockContext = { request: { - body: { - acceptQuote: true + body: bulkTransferRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}) + } + }; + + await handlers['/bulkTransfers'].post(mockContext); + + // check response is correct + expect(mockContext.response.status).toEqual(500); + expect(mockContext.response.body).toBeTruthy(); + expect(mockContext.response.body.message).toEqual('Mock error'); + expect(mockContext.response.body.statusCode) + .toEqual(mockBulkTransferError.bulkTransferState.lastError.mojaloopError.errorInformation.errorCode); + expect(mockContext.response.body.bulkTransferState).toEqual(mockBulkTransferError.bulkTransferState); + }); + + test('uses correct extension list error code for response body statusCode when configured to do so', async () => { + const mockContext = { + request: { + body: bulkTransferRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: { + outboundErrorStatusCodeExtensionKey: 'extErrorKey' // <- tell the handler to use this extensionList item as source of statusCode }, + logger: mockLogger({ app: 'outbound-api-handlers-test'}) + } + }; + + await handlers['/bulkTransfers'].post(mockContext); + + // check response is correct + expect(mockContext.response.status).toEqual(500); + expect(mockContext.response.body).toBeTruthy(); + expect(mockContext.response.body.message).toEqual('Mock error'); + + // in this case, where we have set outboundErrorExtensionKey config we expect the error body statusCode + // property to come from the extensionList item with the corresponding key 'extErrorKey' + expect(mockContext.response.body.statusCode).toEqual('9999'); + expect(mockContext.response.body.bulkTransferState).toEqual(mockBulkTransferError.bulkTransferState); + }); + }); + + describe('POST /bulkQuotes', () => { + test('returns correct error response body when model throws mojaloop error', async () => { + const mockContext = { + request: { + body: bulkQuoteRequest, headers: { 'fspiop-source': 'foo' } @@ -184,23 +293,76 @@ describe('Outbound API handlers:', () => { response: {}, state: { conf: {}, - logger: console, - path: { - params: { - transferId: '12345' - } + logger: mockLogger({ app: 'outbound-api-handlers-test'}) + } + }; + + await handlers['/bulkQuotes'].post(mockContext); + + // check response is correct + expect(mockContext.response.status).toEqual(500); + expect(mockContext.response.body).toBeTruthy(); + expect(mockContext.response.body.message).toEqual('Mock error'); + expect(mockContext.response.body.statusCode) + .toEqual(mockBulkQuoteError.bulkQuoteState.lastError.mojaloopError.errorInformation.errorCode); + expect(mockContext.response.body.bulkQuoteState).toEqual(mockBulkQuoteError.bulkQuoteState); + }); + + test('uses correct extension list error code for response body statusCode when configured to do so', async () => { + const mockContext = { + request: { + body: bulkQuoteRequest, + headers: { + 'fspiop-source': 'foo' } + }, + response: {}, + state: { + conf: { + outboundErrorStatusCodeExtensionKey: 'extErrorKey' // <- tell the handler to use this extensionList item as source of statusCode + }, + logger: mockLogger({ app: 'outbound-api-handlers-test'}) } }; - await handlers['/transfers/{transferId}'].put(mockContext); + await handlers['/bulkQuotes'].post(mockContext); // check response is correct expect(mockContext.response.status).toEqual(500); expect(mockContext.response.body).toBeTruthy(); expect(mockContext.response.body.message).toEqual('Mock error'); - expect(mockContext.response.body.statusCode).toEqual('3204'); - expect(mockContext.response.body.transferState).toEqual(mockError.transferState); + + // in this case, where we have set outboundErrorExtensionKey config we expect the error body statusCode + // property to come from the extensionList item with the corresponding key 'extErrorKey' + expect(mockContext.response.body.statusCode).toEqual('9999'); + expect(mockContext.response.body.bulkQuoteState).toEqual(mockBulkQuoteError.bulkQuoteState); + }); + }); + + describe('POST /requestToPayTransfer', () => { + test('returns correct error response body when model throws mojaloop error', async () => { + const mockContext = { + request: { + body: requestToPayTransferRequest, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}) + } + }; + + await handlers['/requestToPayTransfer'].post(mockContext); + // check response is correct + expect(mockContext.response.status).toEqual(500); + expect(mockContext.response.body).toBeTruthy(); + expect(mockContext.response.body.message).toEqual('Mock error'); + expect(mockContext.response.body.statusCode) + .toEqual(mockRequestToPayTransferError.requestToPayTransferState.lastError.mojaloopError.errorInformation.errorCode); + expect(mockContext.response.body.requestToPayTransferState).toEqual(mockRequestToPayTransferError.requestToPayTransferState); }); }); @@ -216,7 +378,7 @@ describe('Outbound API handlers:', () => { response: {}, state: { conf: {}, - logger: console + logger: mockLogger({ app: 'outbound-api-handlers-test'}) } }; @@ -231,4 +393,245 @@ describe('Outbound API handlers:', () => { }); }); + describe('POST /authorizations', () => { + test('happy flow', async() => { + + const mockContext = { + request: { + body: {the: 'body', toParticipantId: 'pisp', transactionRequestId: '123'}, + headers: { + 'fspiop-source': 'foo' + } + }, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}), + cache: { the: 'mocked cache' } + }, + }; + + // mock state machine + const mockedPSM = { + run: jest.fn(async () => ({ the: 'run response' })) + }; + + const createSpy = jest.spyOn(OutboundAuthorizationsModel, 'create') + .mockImplementationOnce(async () => mockedPSM); + + // invoke handler + await handlers['/authorizations'].post(mockContext); + + // PSM model creation + expect(createSpy).toBeCalledTimes(1); + const request = mockContext.request; + const state = mockContext.state; + const cacheKey = `post_authorizations_${request.body.transactionRequestId}`; + const expectedConfig = { + cache: state.cache, + logger: state.logger, + wso2Auth: state.wso2Auth + }; + expect(createSpy).toBeCalledWith(request.body, cacheKey, expectedConfig); + + // run workflow + expect(mockedPSM.run).toBeCalledTimes(1); + expect(mockedPSM.run.mock.calls[0].length).toBe(0); + + // response + expect(mockContext.response.status).toBe(200); + expect(mockContext.response.body).toEqual({ the: 'run response' }); + }); + }); + + describe('GET /parties/{Type}/{ID}/{SubId}', () => { + test('happy flow', async() => { + + const mockContext = { + request: {}, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}), + cache: { the: 'mocked cache' }, + path: { + params: { + 'Type': 'MSISDN', + 'ID': '1234567890', + 'SubId': 'abcdefgh' + }, + }, + }, + }; + + // mock state machine + const mockedPSM = { + run: jest.fn(async () => ({ the: 'run response' })) + }; + + const createSpy = jest.spyOn(PartiesModel, 'create') + .mockImplementationOnce(async () => mockedPSM); + + // invoke handler + await handlers['/parties/{Type}/{ID}/{SubId}'].get(mockContext); + + // PSM model creation + const state = mockContext.state; + const cacheKey = PartiesModel.channelName('MSISDN', '1234567890', 'abcdefgh'); + const expectedConfig = { + cache: state.cache, + logger: state.logger, + wso2Auth: state.wso2Auth + }; + expect(createSpy).toBeCalledWith({}, cacheKey, expectedConfig); + + // run workflow + expect(mockedPSM.run).toBeCalledWith('MSISDN', '1234567890', 'abcdefgh'); + + // response + expect(mockContext.response.status).toBe(200); + expect(mockContext.response.body).toEqual({ the: 'run response' }); + }); + + test('error flow', async() => { + const mockContext = { + request: {}, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}), + cache: { the: 'mocked cache' }, + path: { + params: { + 'Type': 'MSISDN', + 'ID': '1234567890', + 'SubId': 'abcdefgh' + }, + }, + }, + }; + + // mock state machine + const mockedPSM = { + run: jest.fn(async () => ({ errorInformation: { Iam: 'the-error'} })) + }; + + const createSpy = jest.spyOn(PartiesModel, 'create') + .mockImplementationOnce(async () => mockedPSM); + + // invoke handler + await handlers['/parties/{Type}/{ID}/{SubId}'].get(mockContext); + + // PSM model creation + const state = mockContext.state; + const cacheKey = PartiesModel.channelName('MSISDN', '1234567890', 'abcdefgh'); + const expectedConfig = { + cache: state.cache, + logger: state.logger, + wso2Auth: state.wso2Auth + }; + expect(createSpy).toBeCalledWith({}, cacheKey, expectedConfig); + + // run workflow + expect(mockedPSM.run).toBeCalledWith('MSISDN', '1234567890', 'abcdefgh'); + + // response + expect(mockContext.response.status).toBe(404); + expect(mockContext.response.body).toEqual({ errorInformation: { Iam: 'the-error'} }); + }); + }); + describe('GET /parties/{Type}/{ID}', () => { + test('happy flow', async() => { + + const mockContext = { + request: {}, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}), + cache: { the: 'mocked cache' }, + path: { + params: { + 'Type': 'MSISDN', + 'ID': '1234567890' + }, + }, + }, + }; + + // mock state machine + const mockedPSM = { + run: jest.fn(async () => ({ the: 'run response' })) + }; + + const createSpy = jest.spyOn(PartiesModel, 'create') + .mockImplementationOnce(async () => mockedPSM); + + // invoke handler + await handlers['/parties/{Type}/{ID}'].get(mockContext); + + // PSM model creation + const state = mockContext.state; + const cacheKey = PartiesModel.channelName('MSISDN', '1234567890'); + const expectedConfig = { + cache: state.cache, + logger: state.logger, + wso2Auth: state.wso2Auth + }; + expect(createSpy).toBeCalledWith({}, cacheKey, expectedConfig); + + // run workflow + expect(mockedPSM.run).toBeCalledWith('MSISDN', '1234567890', undefined); + + // response + expect(mockContext.response.status).toBe(200); + expect(mockContext.response.body).toEqual({ the: 'run response' }); + }); + + test('error flow', async() => { + const mockContext = { + request: {}, + response: {}, + state: { + conf: {}, + logger: mockLogger({ app: 'outbound-api-handlers-test'}), + cache: { the: 'mocked cache' }, + path: { + params: { + 'Type': 'MSISDN', + 'ID': '1234567890' + }, + }, + }, + }; + + // mock state machine + const mockedPSM = { + run: jest.fn(async () => ({ errorInformation: { Iam: 'the-error'} })) + }; + + const createSpy = jest.spyOn(PartiesModel, 'create') + .mockImplementationOnce(async () => mockedPSM); + + // invoke handler + await handlers['/parties/{Type}/{ID}'].get(mockContext); + + // PSM model creation + const state = mockContext.state; + const cacheKey = PartiesModel.channelName('MSISDN', '1234567890'); + const expectedConfig = { + cache: state.cache, + logger: state.logger, + wso2Auth: state.wso2Auth + }; + expect(createSpy).toBeCalledWith({}, cacheKey, expectedConfig); + + // run workflow + expect(mockedPSM.run).toBeCalledWith('MSISDN', '1234567890', undefined); + + // response + expect(mockContext.response.status).toBe(404); + expect(mockContext.response.body).toEqual({ errorInformation: { Iam: 'the-error'} }); + }); + }); });